diff --git a/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl b/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl index f75d50776..eddef774a 100644 --- a/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl +++ b/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/apis/concierge/authentication/v1alpha1/types_tls.go.tmpl b/apis/concierge/authentication/v1alpha1/types_tls.go.tmpl index 12231665d..3be891eda 100644 --- a/apis/concierge/authentication/v1alpha1/types_tls.go.tmpl +++ b/apis/concierge/authentication/v1alpha1/types_tls.go.tmpl @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl b/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl index cbe3eeeb0..f05902cb2 100644 --- a/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl +++ b/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl b/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl index 0ee0f0dbf..de976f5c1 100644 --- a/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl +++ b/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl index 95f7da282..d1a6e6278 100644 --- a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl index c84f46dbd..437974778 100644 --- a/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl index 49b49373c..831cd308c 100644 --- a/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index 033513431..678263520 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index 2d51e383a..85a2e9efa 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -16,6 +16,9 @@ rules: - apiGroups: [""] resources: [secrets] verbs: [create, get, list, patch, update, watch, delete] + - apiGroups: [""] + resources: [configmaps] + verbs: [get, list, watch] - apiGroups: - #@ pinnipedDevAPIGroupWithPrefix("config.supervisor") resources: [federationdomains] diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index 3311e31fc..55d1e9a75 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 3ae558150..6c20c0159 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.25/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.25/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.25/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.25/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.25/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.25/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.25/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.25/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index a3f58fc82..d38453a85 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.26/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.26/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.26/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.26/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.26/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.26/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.26/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.26/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 3fd81787a..8f76bded1 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.27/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.27/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.27/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.27/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.27/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.27/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.27/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.27/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index fb7366e47..de0de6e43 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.28/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.28/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.28/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.28/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.28/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.28/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.28/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.28/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.29/README.adoc b/generated/1.29/README.adoc index 120952689..4dc6f2bf7 100644 --- a/generated/1.29/README.adoc +++ b/generated/1.29/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.29/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.29/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.29/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.29/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.29/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.29/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.29/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.29/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.29/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.29/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.29/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.29/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.29/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.29/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.29/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.29/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.29/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.29/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.29/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.29/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml index d43c7406d..b7390da91 100644 --- a/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/1.30/README.adoc b/generated/1.30/README.adoc index 337aacd2a..81cc0a298 100644 --- a/generated/1.30/README.adoc +++ b/generated/1.30/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/1.30/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.30/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/1.30/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.30/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/1.30/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.30/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/1.30/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/1.30/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.30/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.30/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/1.30/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/1.30/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/1.30/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.30/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/1.30/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.30/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/1.30/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.30/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/1.30/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.30/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/1.30/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.30/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/1.30/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.30/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index d59fcb783..f707ed4a8 100644 --- a/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .spec.audience name: Audience type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -92,6 +95,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - audience diff --git a/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index 4ccd53770..7285fee36 100644 --- a/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .spec.endpoint name: Endpoint type: string + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -63,6 +66,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - endpoint diff --git a/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 033513431..678263520 100644 --- a/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -143,8 +143,9 @@ spec: Type is "string", and is otherwise ignored. type: string type: - description: Type determines the type of the constant, - and indicates which other field should be non-empty. + description: |- + Type determines the type of the constant, and indicates which other field should be non-empty. + Allowed values are "string" or "stringList". enum: - string - stringList @@ -262,8 +263,9 @@ spec: an authentication attempt. When empty, a default message will be used. type: string type: - description: Type determines the type of the expression. - It must be one of the supported types. + description: |- + Type determines the type of the expression. It must be one of the supported types. + Allowed values are "policy/v1", "username/v1", or "groups/v1". enum: - policy/v1 - username/v1 diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 062251102..a9f4ee414 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -170,6 +170,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml index f93108700..d14c6773d 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -89,7 +89,11 @@ spec: policy: default: OnlyUsersFromAllowedOrganizations description: |- - Policy must be set to "AllGitHubUsers" if allowed is empty. + Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + Defaults to "OnlyUsersFromAllowedOrganizations". + + + Must be set to "AllGitHubUsers" if the allowed field is empty. This field only exists to ensure that Pinniped administrators are aware that an empty list of @@ -225,6 +229,39 @@ spec: bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object type: object required: diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 711e9a754..463351de6 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -161,6 +161,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object userSearch: description: UserSearch contains the configuration for searching for diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index acfca1573..33e15403c 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -211,6 +211,39 @@ spec: description: X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. type: string + certificateAuthorityDataSource: + description: |- + Reference to a CA bundle in a secret or a configmap. + Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + properties: + key: + description: |- + Key is the key name within the secret or configmap from which to read the CA bundle. + The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + certificate bundle. + minLength: 1 + type: string + kind: + description: |- + Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + Allowed values are "Secret" or "ConfigMap". + "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name is the resource name of the secret or configmap from which to read the CA bundle. + The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + minLength: 1 + type: string + required: + - key + - kind + - name + type: object type: object required: - client diff --git a/generated/latest/README.adoc b/generated/latest/README.adoc index 337aacd2a..81cc0a298 100644 --- a/generated/latest/README.adoc +++ b/generated/latest/README.adoc @@ -23,6 +23,43 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-jwtauthenticator"] ==== JWTAuthenticator @@ -125,7 +162,7 @@ username from the JWT token. When not specified, it will default to "username". [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for configuring TLS on various authenticators. +TLSSpec provides TLS configuration on various authenticators. .Appears In: **** @@ -137,6 +174,8 @@ Configuration for configuring TLS on various authenticators. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-concierge-authentication-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== @@ -503,6 +542,7 @@ ImpersonationProxyInfo describes the parameters for the impersonation proxy on t ==== ImpersonationProxyMode (string) ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +Allowed values are "auto", "enabled", or "disabled". .Appears In: **** @@ -539,6 +579,7 @@ This is not supported on all cloud providers. + ==== ImpersonationProxyServiceType (string) ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +Allowed values are "LoadBalancer", "ClusterIP", or "None". .Appears In: **** @@ -928,6 +969,7 @@ the transform expressions. This is a union type, and Type is the discriminator f | Field | Description | *`name`* __string__ | Name determines the name of the constant. It must be a valid identifier name. + | *`type`* __string__ | Type determines the type of the constant, and indicates which other field should be non-empty. + +Allowed values are "string" or "stringList". + | *`stringValue`* __string__ | StringValue should hold the value when Type is "string", and is otherwise ignored. + | *`stringListValue`* __string array__ | StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + |=== @@ -994,6 +1036,7 @@ FederationDomainTransformsExpression defines a transform expression. |=== | Field | Description | *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. + +Allowed values are "policy/v1", "username/v1", or "groups/v1". + | *`expression`* __string__ | Expression is a CEL expression that will be evaluated based on the Type during an authentication. + | *`message`* __string__ | Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects + an authentication attempt. When empty, a default message will be used. + @@ -1645,6 +1688,43 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind"] +==== CertificateAuthorityDataSourceKind (string) + +CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec"] +==== CertificateAuthorityDataSourceSpec + +CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcekind[$$CertificateAuthorityDataSourceKind$$]__ | Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + +Allowed values are "Secret" or "ConfigMap". + +"ConfigMap" uses a Kubernetes configmap to source CA Bundles. + +"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + +| *`name`* __string__ | Name is the resource name of the secret or configmap from which to read the CA bundle. + +The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + +| *`key`* __string__ | Key is the key name within the secret or configmap from which to read the CA bundle. + +The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + +certificate bundle. + +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig"] ==== GitHubAPIConfig @@ -1890,7 +1970,11 @@ GitHubIdentityProviderStatus is the status of an GitHub identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + +Defaults to "OnlyUsersFromAllowedOrganizations". + + + +Must be set to "AllGitHubUsers" if the allowed field is empty. + This field only exists to ensure that Pinniped administrators are aware that an empty list of + @@ -2401,6 +2485,8 @@ TLSSpec provides TLS configuration for identity provider integration. |=== | Field | Description | *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + +| *`certificateAuthorityDataSource`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-certificateauthoritydatasourcespec[$$CertificateAuthorityDataSourceSpec$$]__ | Reference to a CA bundle in a secret or a configmap. + +Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + |=== diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index f75d50776..eddef774a 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -79,6 +79,7 @@ type JWTTokenClaims struct { // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type JWTAuthenticator struct { diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_tls.go index 12231665d..3be891eda 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/types_tls.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_tls.go @@ -1,11 +1,47 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for configuring TLS on various authenticators. +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// TLSSpec provides TLS configuration on various authenticators. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go index cbe3eeeb0..f05902cb2 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -50,6 +50,7 @@ type WebhookAuthenticatorSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster // +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type WebhookAuthenticator struct { diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go index 5d36cf81b..1e64ee699 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { *out = *in @@ -81,7 +97,7 @@ func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -138,6 +154,11 @@ func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } @@ -218,7 +239,7 @@ func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go index 0ee0f0dbf..de976f5c1 100644 --- a/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -49,6 +49,7 @@ type CredentialIssuerSpec struct { } // ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// Allowed values are "auto", "enabled", or "disabled". // // +kubebuilder:validation:Enum=auto;enabled;disabled type ImpersonationProxyMode string @@ -65,6 +66,7 @@ const ( ) // ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// Allowed values are "LoadBalancer", "ClusterIP", or "None". // // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None type ImpersonationProxyServiceType string diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go index 95f7da282..d1a6e6278 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -55,6 +55,7 @@ type FederationDomainTransformsConstant struct { Name string `json:"name"` // Type determines the type of the constant, and indicates which other field should be non-empty. + // Allowed values are "string" or "stringList". // +kubebuilder:validation:Enum=string;stringList Type string `json:"type"` @@ -70,6 +71,7 @@ type FederationDomainTransformsConstant struct { // FederationDomainTransformsExpression defines a transform expression. type FederationDomainTransformsExpression struct { // Type determines the type of the expression. It must be one of the supported types. + // Allowed values are "policy/v1", "username/v1", or "groups/v1". // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 Type string `json:"type"` diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go index c84f46dbd..437974778 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -167,7 +167,10 @@ type GitHubClientSpec struct { } type GitHubOrganizationsSpec struct { - // Policy must be set to "AllGitHubUsers" if allowed is empty. + // Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers". + // Defaults to "OnlyUsersFromAllowedOrganizations". + // + // Must be set to "AllGitHubUsers" if the allowed field is empty. // // This field only exists to ensure that Pinniped administrators are aware that an empty list of // allowedOrganizations means all GitHub users are allowed to log in. diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go index 49b49373c..831cd308c 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go @@ -3,9 +3,45 @@ package v1alpha1 +// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles. +// +// +kubebuilder:validation:Enum=Secret;ConfigMap +type CertificateAuthorityDataSourceKind string + +const ( + // CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles. + CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap") + + // CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles. + // Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque. + CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret") +) + +// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification. +type CertificateAuthorityDataSourceSpec struct { + // Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap. + // Allowed values are "Secret" or "ConfigMap". + // "ConfigMap" uses a Kubernetes configmap to source CA Bundles. + // "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles. + Kind CertificateAuthorityDataSourceKind `json:"kind"` + // Name is the resource name of the secret or configmap from which to read the CA bundle. + // The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Key is the key name within the secret or configmap from which to read the CA bundle. + // The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded + // certificate bundle. + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` + // Reference to a CA bundle in a secret or a configmap. + // Any changes to the CA bundle in the secret or configmap will be dynamically reloaded. + // +optional + CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"` } diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index e48860e82..395c8f0fb 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -129,7 +129,7 @@ func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectory if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -203,6 +203,22 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateAuthorityDataSourceSpec) DeepCopyInto(out *CertificateAuthorityDataSourceSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAuthorityDataSourceSpec. +func (in *CertificateAuthorityDataSourceSpec) DeepCopy() *CertificateAuthorityDataSourceSpec { + if in == nil { + return nil + } + out := new(CertificateAuthorityDataSourceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { *out = *in @@ -214,7 +230,7 @@ func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -534,7 +550,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } out.Bind = in.Bind out.UserSearch = in.UserSearch @@ -740,7 +756,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSpec) - **out = **in + (*in).DeepCopyInto(*out) } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.Claims.DeepCopyInto(&out.Claims) @@ -800,6 +816,11 @@ func (in *Parameter) DeepCopy() *Parameter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in + if in.CertificateAuthorityDataSource != nil { + in, out := &in.CertificateAuthorityDataSource, &out.CertificateAuthorityDataSource + *out = new(CertificateAuthorityDataSourceSpec) + **out = **in + } return } diff --git a/internal/controller/authenticator/authncache/cache.go b/internal/controller/authenticator/authncache/cache.go index 9e2b15011..e62605fe3 100644 --- a/internal/controller/authenticator/authncache/cache.go +++ b/internal/controller/authenticator/authncache/cache.go @@ -36,6 +36,7 @@ type Key struct { type Value interface { authenticator.Token + Close() } // New returns an empty cache. @@ -45,21 +46,31 @@ func New() *Cache { // Get an authenticator by key. func (c *Cache) Get(key Key) Value { - res, _ := c.cache.Load(key) - if res == nil { + v, _ := c.cache.Load(key) + if v == nil { return nil } - return res.(Value) + return v.(Value) } -// Store an authenticator into the cache. +// Store an authenticator into the cache. If overwriting a value in the cache, closes the overwritten value. func (c *Cache) Store(key Key, value Value) { - c.cache.Store(key, value) + previousValue, _ := c.cache.Swap(key, value) + // Wait until after it has been overwritten in the cache to close it, to ensure that it is only closed + // after it is not available for cache reads anymore. + if previousValue != nil { + previousValue.(Value).Close() + } } -// Delete an authenticator from the cache. +// Delete an authenticator from the cache. Closes the authenticator after removing it from the cache. func (c *Cache) Delete(key Key) { - c.cache.Delete(key) + deletedValue, _ := c.cache.LoadAndDelete(key) + // Wait until after it has been removed from the cache to close it, to ensure that it is only closed + // after it is not available for cache reads anymore. + if deletedValue != nil { + deletedValue.(Value).Close() + } } // Keys currently stored in the cache. diff --git a/internal/controller/authenticator/authncache/cache_test.go b/internal/controller/authenticator/authncache/cache_test.go index 13c6de76b..68520588a 100644 --- a/internal/controller/authenticator/authncache/cache_test.go +++ b/internal/controller/authenticator/authncache/cache_test.go @@ -19,35 +19,52 @@ import ( authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" loginapi "go.pinniped.dev/generated/latest/apis/concierge/login" - "go.pinniped.dev/internal/mocks/mocktokenauthenticator" + "go.pinniped.dev/internal/mocks/mockcachevalue" ) func TestCache(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - defer ctrl.Finish() + t.Cleanup(func() { + ctrl.Finish() + }) cache := New() require.NotNil(t, cache) key1 := Key{Name: "authenticator-one"} - mockToken1 := mocktokenauthenticator.NewMockToken(ctrl) - cache.Store(key1, mockToken1) - require.Equal(t, mockToken1, cache.Get(key1)) + mockValue1 := mockcachevalue.NewMockValue(ctrl) + require.Nil(t, cache.Get(key1)) + cache.Store(key1, mockValue1) + require.Equal(t, mockValue1, cache.Get(key1)) require.Equal(t, 1, len(cache.Keys())) key2 := Key{Name: "authenticator-two"} - mockToken2 := mocktokenauthenticator.NewMockToken(ctrl) - cache.Store(key2, mockToken2) - require.Equal(t, mockToken2, cache.Get(key2)) + mockValue2 := mockcachevalue.NewMockValue(ctrl) + cache.Store(key2, mockValue2) + require.Equal(t, mockValue2, cache.Get(key2)) require.Equal(t, 2, len(cache.Keys())) + // Assert that Close() has not been called yet, and it should be called by the end of the test. + mockValue1.EXPECT().Close().Times(1) + mockValue2.EXPECT().Close().Times(1) + for _, key := range cache.Keys() { cache.Delete(key) } require.Zero(t, len(cache.Keys())) + key3 := Key{Name: "authenticator-three"} + mockValue3 := mockcachevalue.NewMockValue(ctrl) + cache.Store(key3, mockValue3) + require.Equal(t, mockValue3, cache.Get(key3)) + require.Equal(t, 1, len(cache.Keys())) + mockValue4 := mockcachevalue.NewMockValue(ctrl) + // Assert that Close() has not been called yet, and it should be called by the end of the test. + mockValue3.EXPECT().Close().Times(1) + cache.Store(key3, mockValue4) // overwrite + // Fill the cache back up with a fixed set of keys, but inserted in shuffled order. keysInExpectedOrder := []Key{ {APIGroup: "a", Kind: "a", Name: "a"}, @@ -92,7 +109,7 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) { mockCache := func(t *testing.T, res *authenticator.Response, authenticated bool, err error) *Cache { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) - m := mocktokenauthenticator.NewMockToken(ctrl) + m := mockcachevalue.NewMockValue(ctrl) m.EXPECT().AuthenticateToken(audienceFreeContext{}, validRequest.Spec.Token).Return(res, authenticated, err) c := New() c.Store(validRequestKey, m) @@ -137,7 +154,7 @@ func TestAuthenticateTokenCredentialRequest(t *testing.T) { t.Run("context is cancelled", func(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) - m := mocktokenauthenticator.NewMockToken(ctrl) + m := mockcachevalue.NewMockValue(ctrl) m.EXPECT().AuthenticateToken(gomock.Any(), validRequest.Spec.Token).DoAndReturn( func(ctx context.Context, token string) (*authenticator.Response, bool, error) { select { diff --git a/internal/controller/authenticator/cachecleaner/cachecleaner_test.go b/internal/controller/authenticator/cachecleaner/cachecleaner_test.go index 3602c0b21..214e65f30 100644 --- a/internal/controller/authenticator/cachecleaner/cachecleaner_test.go +++ b/internal/controller/authenticator/cachecleaner/cachecleaner_test.go @@ -155,7 +155,6 @@ func TestController(t *testing.T) { defer cancel() informers.Start(ctx.Done()) - informers.WaitForCacheSync(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) syncCtx := controllerlib.Context{ diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index ed1acf2bb..297c0fbd1 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -7,25 +7,26 @@ package jwtcachefiller import ( "context" - "crypto/x509" "errors" "fmt" "net/http" "net/url" "reflect" + "slices" "strings" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/go-jose/go-jose/v4" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" utilerrors "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" + corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/utils/clock" "k8s.io/utils/ptr" @@ -37,6 +38,7 @@ import ( pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator" "go.pinniped.dev/internal/controller/authenticator/authncache" "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/plog" @@ -45,25 +47,19 @@ import ( const ( controllerName = "jwtcachefiller-controller" - typeReady = "Ready" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeIssuerURLValid = "IssuerURLValid" - typeDiscoveryValid = "DiscoveryURLValid" - typeJWKSURLValid = "JWKSURLValid" - typeJWKSFetchValid = "JWKSFetchValid" - typeAuthenticatorValid = "AuthenticatorValid" + typeReady = "Ready" + typeIssuerURLValid = "IssuerURLValid" + typeDiscoveryValid = "DiscoveryURLValid" + typeJWKSURLValid = "JWKSURLValid" + typeJWKSFetchValid = "JWKSFetchValid" + typeAuthenticatorValid = "AuthenticatorValid" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" - reasonInvalidIssuerURL = "InvalidIssuerURL" reasonInvalidIssuerURLScheme = "InvalidIssuerURLScheme" reasonInvalidIssuerURLFragment = "InvalidIssuerURLContainsFragment" reasonInvalidIssuerURLQueryParams = "InvalidIssuerURLContainsQueryParams" reasonInvalidIssuerURLContainsWellKnownEndpoint = "InvalidIssuerURLContainsWellKnownEndpoint" reasonInvalidProviderJWKSURL = "InvalidProviderJWKSURL" reasonInvalidProviderJWKSURLScheme = "InvalidProviderJWKSURLScheme" - reasonInvalidTLSConfiguration = "InvalidTLSConfiguration" reasonInvalidDiscoveryProbe = "InvalidDiscoveryProbe" reasonInvalidAuthenticator = "InvalidAuthenticator" reasonInvalidCouldNotFetchJWKS = "InvalidCouldNotFetchJWKS" @@ -114,8 +110,9 @@ type tokenAuthenticatorCloser interface { type cachedJWTAuthenticator struct { authenticator.Token - spec *authenticationv1alpha1.JWTAuthenticatorSpec - cancel context.CancelFunc + issuer string + caBundleHash tlsconfigutil.CABundleHash + cancel context.CancelFunc } func (c *cachedJWTAuthenticator) Close() { @@ -129,9 +126,13 @@ var _ tokenAuthenticatorCloser = (*cachedJWTAuthenticator)(nil) // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache. func New( + namespace string, cache *authncache.Cache, client conciergeclientset.Interface, jwtAuthenticators authinformers.JWTAuthenticatorInformer, + secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, clock clock.Clock, log plog.Logger, ) controllerlib.Controller { @@ -139,24 +140,46 @@ func New( controllerlib.Config{ Name: controllerName, Syncer: &jwtCacheFillerController{ + namespace: namespace, cache: cache, client: client, jwtAuthenticators: jwtAuthenticators, + secretInformer: secretInformer, + configMapInformer: configMapInformer, clock: clock, log: log.WithName(controllerName), }, }, - controllerlib.WithInformer( + withInformer( jwtAuthenticators, - pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue(), + ), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) } type jwtCacheFillerController struct { + namespace string cache *authncache.Cache jwtAuthenticators authinformers.JWTAuthenticatorInformer + secretInformer corev1informers.SecretInformer + configMapInformer corev1informers.ConfigMapInformer client conciergeclientset.Interface clock clock.Clock log plog.Logger @@ -164,53 +187,127 @@ type jwtCacheFillerController struct { // Sync implements controllerlib.Syncer. func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error { - obj, err := c.jwtAuthenticators.Lister().Get(ctx.Key.Name) - if err != nil && apierrors.IsNotFound(err) { - c.log.Info("Sync() found that the JWTAuthenticator does not exist yet or was deleted") - return nil - } + jwtAuthenticators, err := c.jwtAuthenticators.Lister().List(labels.Everything()) if err != nil { - // no unit test for this failure - return fmt.Errorf("failed to get JWTAuthenticator %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) + return err } + if len(jwtAuthenticators) == 0 { + c.log.Info("No JWTAuthenticators found") + return nil + } + + // Sort them by name so that order is predictable and therefore output is consistent for tests and logs. + slices.SortStableFunc(jwtAuthenticators, func(a, b *authenticationv1alpha1.JWTAuthenticator) int { + return strings.Compare(a.Name, b.Name) + }) + + var errs []error + for _, jwtAuthenticator := range jwtAuthenticators { + err = c.syncIndividualJWTAuthenticator(ctx.Context, jwtAuthenticator) + if err != nil { + errs = append(errs, fmt.Errorf("error for JWTAuthenticator %s: %w", jwtAuthenticator.Name, err)) + } + } + return utilerrors.NewAggregate(errs) +} + +func (c *jwtCacheFillerController) syncIndividualJWTAuthenticator(ctx context.Context, jwtAuthenticator *authenticationv1alpha1.JWTAuthenticator) error { cacheKey := authncache.Key{ APIGroup: authenticationv1alpha1.GroupName, Kind: "JWTAuthenticator", - Name: ctx.Key.Name, + Name: jwtAuthenticator.Name, } + logger := c.log.WithValues( + "jwtAuthenticator", jwtAuthenticator.Name, + "issuer", jwtAuthenticator.Spec.Issuer) + + var errs []error + conditions := make([]*metav1.Condition, 0) + var newJWTAuthenticatorForCache *cachedJWTAuthenticator + + caBundle, conditions, tlsBundleOk := c.validateTLSBundle(jwtAuthenticator.Spec.TLS, conditions) + + conditions, issuerOk := c.validateIssuer(jwtAuthenticator.Spec.Issuer, conditions) + okSoFar := tlsBundleOk && issuerOk + // Only revalidate and update the cache if the cached authenticator is different from the desired authenticator. - // There is no need to repeat validations for a spec that was already successfully validated. We are making a - // design decision to avoid repeating the validation which dials the server, even though the server's TLS - // configuration could have changed, because it is also possible that the network could be flaky. We are choosing - // to prefer to keep the authenticator cached (available for end-user auth attempts) during times of network flakes - // rather than trying to show the most up-to-date status possible. These validations are for administrator - // convenience at the time of a configuration change, to catch typos and blatant misconfigurations, rather - // than to constantly monitor for external issues. - var jwtAuthenticatorFromCache *cachedJWTAuthenticator - if valueFromCache := c.cache.Get(cacheKey); valueFromCache != nil { - jwtAuthenticatorFromCache = c.cacheValueAsJWTAuthenticator(valueFromCache) - if jwtAuthenticatorFromCache != nil && reflect.DeepEqual(jwtAuthenticatorFromCache.spec, &obj.Spec) { - c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer). - Info("actual jwt authenticator and desired jwt authenticator are the same") - // Stop, no more work to be done. This authenticator is already validated and cached. - return nil - } + // There is no need to repeat connection probe validations for a URL and CA bundle combination that was already + // successfully validated. We are making a design decision to avoid repeating the validation which dials the server, + // even though the server's TLS configuration could have changed, because it is also possible that the network + // could be flaky. We are choosing to prefer to keep the authenticator cached (available for end-user auth attempts) + // during times of network flakes rather than trying to show the most up-to-date status possible. These validations + // are for administrator convenience at the time of a configuration change, to catch typos and blatant + // misconfigurations, rather than to constantly monitor for external issues. + foundAuthenticatorInCache, previouslyValidatedWithSameEndpointAndBundle := c.havePreviouslyValidated( + cacheKey, jwtAuthenticator.Spec.Issuer, tlsBundleOk, caBundle.Hash(), logger) + if previouslyValidatedWithSameEndpointAndBundle { + // Because the authenticator was previously cached, that implies that the following conditions were + // previously validated. These are the expensive validations to repeat, so skip them this time. + // However, the status may be lagging behind due to the informer cache being slow to catch up + // after previous status updates, so always calculate the new status conditions again and check + // if they need to be updated. + logger.Info("cached jwt authenticator and desired jwt authenticator are the same: already cached, so skipping validations") + conditions = append(conditions, + successfulDiscoveryValidCondition(), + successfulJWKSURLValidCondition(), + successfulJWKSFetchValidCondition(), + successfulAuthenticatorValidCondition(), + ) + } else { + // Run all remaining validations. + a, moreConditions, moreErrs := c.doExpensiveValidations(jwtAuthenticator, caBundle, okSoFar) + newJWTAuthenticatorForCache = a + conditions = append(conditions, moreConditions...) + errs = append(errs, moreErrs...) } - conditions := make([]*metav1.Condition, 0) + authenticatorValid := !conditionsutil.HadErrorCondition(conditions) + + // If we calculated a failed status condition, then remove it from the cache even before we try to write + // the status, because writing the status can fail for various reasons. + if !authenticatorValid { + // The authenticator was determined to be invalid. Remove it from the cache, in case it was previously + // validated and cached. Do not allow an old, previously validated spec of the authenticator to continue + // being used for authentication. + c.cache.Delete(cacheKey) + logger.Info("invalid jwt authenticator", + "removedFromCache", foundAuthenticatorInCache) + } + + // Always try to update the status, even when we found it in the authenticator cache. + updateErr := c.updateStatus(ctx, jwtAuthenticator, conditions, logger) + errs = append(errs, updateErr) + + // Only add/update this authenticator to the cache when we have a new one and the status update succeeded. + if newJWTAuthenticatorForCache != nil && authenticatorValid && updateErr == nil { + c.cache.Store(cacheKey, newJWTAuthenticatorForCache) + logger.Info("added or updated jwt authenticator in cache", + "isOverwrite", foundAuthenticatorInCache) + } + + // 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 utilerrors.NewAggregate(errs) +} + +func (c *jwtCacheFillerController) doExpensiveValidations( + jwtAuthenticator *authenticationv1alpha1.JWTAuthenticator, + caBundle *tlsconfigutil.CABundle, + okSoFar bool, +) (*cachedJWTAuthenticator, []*metav1.Condition, []error) { + var conditions []*metav1.Condition var errs []error - rootCAs, conditions, tlsOk := c.validateTLSBundle(obj.Spec.TLS, conditions) - _, conditions, issuerOk := c.validateIssuer(obj.Spec.Issuer, conditions) - okSoFar := tlsOk && issuerOk - - client := phttp.Default(rootCAs) + client := phttp.Default(caBundle.CertPool()) client.Timeout = 30 * time.Second // copied from Kube OIDC code coreOSCtx := coreosoidc.ClientContext(context.Background(), client) - pJSON, provider, conditions, providerErr := c.validateProviderDiscovery(coreOSCtx, obj.Spec.Issuer, conditions, okSoFar) + pJSON, provider, conditions, providerErr := c.validateProviderDiscovery(coreOSCtx, jwtAuthenticator.Spec.Issuer, conditions, okSoFar) errs = append(errs, providerErr) okSoFar = okSoFar && providerErr == nil @@ -224,89 +321,77 @@ func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error { newJWTAuthenticatorForCache, conditions, err := c.newCachedJWTAuthenticator( client, - obj.Spec.DeepCopy(), // deep copy to avoid caching original object + &jwtAuthenticator.Spec, keySet, + caBundle.Hash(), conditions, okSoFar) errs = append(errs, err) - if conditionsutil.HadErrorCondition(conditions) { - // The authenticator was determined to be invalid. Remove it from the cache, in case it was previously - // validated and cached. Do not allow an old, previously validated spec of the authenticator to continue - // being used for authentication. - c.cache.Delete(cacheKey) - } else { - c.cache.Store(cacheKey, newJWTAuthenticatorForCache) - c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer). - Info("added new jwt authenticator") - } - - // In case we just overwrote or deleted the authenticator from the cache, clean up the old instance - // to avoid leaking goroutines. It's safe to call Close() on nil. We avoid calling Close() until it is - // removed from the cache, because we do not want any end-user authentications to use a closed authenticator. - jwtAuthenticatorFromCache.Close() - - 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 utilerrors.NewAggregate(errs) + return newJWTAuthenticatorForCache, conditions, errs } -func (c *jwtCacheFillerController) cacheValueAsJWTAuthenticator(value authncache.Value) *cachedJWTAuthenticator { +func (c *jwtCacheFillerController) havePreviouslyValidated( + cacheKey authncache.Key, + issuer string, + tlsBundleOk bool, + caBundleHash tlsconfigutil.CABundleHash, + logger plog.Logger, +) (bool, bool) { + var authenticatorFromCache *cachedJWTAuthenticator + valueFromCache := c.cache.Get(cacheKey) + if valueFromCache == nil { + return false, false + } + authenticatorFromCache = c.cacheValueAsJWTAuthenticator(valueFromCache, logger) + if authenticatorFromCache == nil { + return false, false + } + if authenticatorFromCache.issuer == issuer && + tlsBundleOk && // if there was any error while validating the latest CA bundle, then do not consider it previously validated + authenticatorFromCache.caBundleHash.Equal(caBundleHash) { + return true, true + } + return true, false // found the authenticator, but it had not been previously validated with these same settings +} + +func (c *jwtCacheFillerController) cacheValueAsJWTAuthenticator(value authncache.Value, logger plog.Logger) *cachedJWTAuthenticator { jwtAuthenticator, ok := value.(*cachedJWTAuthenticator) if !ok { actualType := "" if t := reflect.TypeOf(value); t != nil { actualType = t.String() } - c.log.WithValues("actualType", actualType).Info("wrong JWT authenticator type in cache") + logger.Info("wrong JWT authenticator type in cache", + "actualType", actualType) return nil } return jwtAuthenticator } -func (c *jwtCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) { - rootCAs, _, err := pinnipedcontroller.BuildCertPoolAuth(tlsSpec) - if err != nil { - msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error()) - conditions = append(conditions, &metav1.Condition{ - Type: typeTLSConfigurationValid, - Status: metav1.ConditionFalse, - Reason: reasonInvalidTLSConfiguration, - Message: msg, - }) - return rootCAs, conditions, false - } +func (c *jwtCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*tlsconfigutil.CABundle, []*metav1.Condition, bool) { + condition, caBundle := tlsconfigutil.ValidateTLSConfig( + tlsconfigutil.TLSSpecForConcierge(tlsSpec), + "spec.tls", + c.namespace, + c.secretInformer, + c.configMapInformer) - 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 + conditions = append(conditions, condition) + return caBundle, conditions, condition.Status == metav1.ConditionTrue } -func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) { +func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*metav1.Condition) ([]*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, + Reason: conditionsutil.ReasonInvalidIssuerURL, Message: msg, }) - return nil, conditions, false + return conditions, false } if issuerURL.Scheme != "https" { @@ -317,7 +402,7 @@ func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*m Reason: reasonInvalidIssuerURLScheme, Message: msg, }) - return nil, conditions, false + return conditions, false } if strings.HasSuffix(issuerURL.Path, "/.well-known/openid-configuration") { @@ -328,7 +413,7 @@ func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*m Reason: reasonInvalidIssuerURLContainsWellKnownEndpoint, Message: msg, }) - return nil, conditions, false + return conditions, false } if len(issuerURL.Query()) != 0 { @@ -339,7 +424,7 @@ func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*m Reason: reasonInvalidIssuerURLQueryParams, Message: msg, }) - return nil, conditions, false + return conditions, false } if issuerURL.Fragment != "" { @@ -350,16 +435,25 @@ func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*m Reason: reasonInvalidIssuerURLFragment, Message: msg, }) - return nil, conditions, false + return conditions, false } conditions = append(conditions, &metav1.Condition{ Type: typeIssuerURLValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "issuer is a valid URL", }) - return issuerURL, conditions, true + return conditions, true +} + +func successfulDiscoveryValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeDiscoveryValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "discovery performed successfully", + } } func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context, issuer string, conditions []*metav1.Condition, prereqOk bool) (*providerJSON, *coreosoidc.Provider, []*metav1.Condition, error) { @@ -367,7 +461,7 @@ func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context conditions = append(conditions, &metav1.Condition{ Type: typeDiscoveryValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return nil, nil, conditions, nil @@ -387,22 +481,25 @@ func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context // resync err, may be machine or other types of non-config error return nil, nil, conditions, fmt.Errorf("%s: %s", errText, err) } - msg := "discovery performed successfully" - conditions = append(conditions, &metav1.Condition{ - Type: typeDiscoveryValid, - Status: metav1.ConditionTrue, - Reason: reasonSuccess, - Message: msg, - }) + conditions = append(conditions, successfulDiscoveryValidCondition()) return pJSON, provider, conditions, nil } +func successfulJWKSURLValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeJWKSURLValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "jwks_uri is a valid URL", + } +} + 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, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return "", conditions, nil @@ -447,15 +544,19 @@ func (c *jwtCacheFillerController) validateProviderJWKSURL(provider *coreosoidc. 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", - }) + conditions = append(conditions, successfulJWKSURLValidCondition()) return pJSON.JWKSURL, conditions, nil } +func successfulJWKSFetchValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeJWKSFetchValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "successfully fetched jwks", + } +} + // 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 @@ -465,7 +566,7 @@ func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksUR conditions = append(conditions, &metav1.Condition{ Type: typeJWKSFetchValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return nil, conditions, nil @@ -506,12 +607,7 @@ func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksUR // 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", - }) + conditions = append(conditions, successfulJWKSFetchValidCondition()) return keySet, conditions, nil } @@ -521,19 +617,35 @@ func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksUR conditions = append(conditions, &metav1.Condition{ Type: typeJWKSFetchValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msg, }) return nil, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr) } +func successfulAuthenticatorValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeAuthenticatorValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "authenticator initialized", + } +} + // newCachedJWTAuthenticator creates a jwt authenticator from the provided spec. -func (c *jwtCacheFillerController) newCachedJWTAuthenticator(client *http.Client, spec *authenticationv1alpha1.JWTAuthenticatorSpec, keySet *coreosoidc.RemoteKeySet, conditions []*metav1.Condition, prereqOk bool) (*cachedJWTAuthenticator, []*metav1.Condition, error) { +func (c *jwtCacheFillerController) newCachedJWTAuthenticator( + client *http.Client, + spec *authenticationv1alpha1.JWTAuthenticatorSpec, + keySet *coreosoidc.RemoteKeySet, + caBundleHash tlsconfigutil.CABundleHash, + conditions []*metav1.Condition, + prereqOk bool, +) (*cachedJWTAuthenticator, []*metav1.Condition, error) { if !prereqOk { conditions = append(conditions, &metav1.Condition{ Type: typeAuthenticatorValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return nil, conditions, nil @@ -588,17 +700,12 @@ func (c *jwtCacheFillerController) newCachedJWTAuthenticator(client *http.Client // resync err, lots of possible issues that may or may not be machine related return nil, conditions, fmt.Errorf("%s: %w", errText, err) } - msg := "authenticator initialized" - conditions = append(conditions, &metav1.Condition{ - Type: typeAuthenticatorValid, - Status: metav1.ConditionTrue, - Reason: reasonSuccess, - Message: msg, - }) + conditions = append(conditions, successfulAuthenticatorValidCondition()) return &cachedJWTAuthenticator{ - Token: oidcAuthenticator, - spec: spec, - cancel: cancel, + Token: oidcAuthenticator, + issuer: spec.Issuer, + caBundleHash: caBundleHash, + cancel: cancel, }, conditions, nil } @@ -606,6 +713,7 @@ func (c *jwtCacheFillerController) updateStatus( ctx context.Context, original *authenticationv1alpha1.JWTAuthenticator, conditions []*metav1.Condition, + logger plog.Logger, ) error { updated := original.DeepCopy() @@ -614,7 +722,7 @@ func (c *jwtCacheFillerController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionFalse, - Reason: reasonNotReady, + Reason: conditionsutil.ReasonNotReady, Message: "the JWTAuthenticator is not ready: see other conditions for details", }) } else { @@ -622,7 +730,7 @@ func (c *jwtCacheFillerController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the JWTAuthenticator is ready", }) } @@ -631,13 +739,19 @@ func (c *jwtCacheFillerController) updateStatus( conditions, original.Generation, &updated.Status.Conditions, - plog.New().WithName(controllerName), + logger, metav1.NewTime(c.clock.Now()), ) if equality.Semantic.DeepEqual(original, updated) { + logger.Debug("choosing to not update the jwtauthenticator status since there is no update to make", + "phase", updated.Status.Phase) return nil } _, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + if err == nil { + logger.Debug("jwtauthenticator status successfully updated", + "phase", updated.Status.Phase) + } return err } diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go index cdc207b1b..1f9dbd693 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go @@ -12,6 +12,7 @@ import ( "crypto/rsa" "crypto/x509" _ "embed" + "encoding/base64" "encoding/json" "encoding/pem" "errors" @@ -26,12 +27,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" + k8sinformers "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" @@ -39,11 +45,12 @@ import ( conciergefake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake" conciergeinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions" "go.pinniped.dev/internal/controller/authenticator/authncache" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/crypto/ptls" + "go.pinniped.dev/internal/mocks/mockcachevalue" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/testutil/conciergetestutil" "go.pinniped.dev/internal/testutil/conditionstestutil" "go.pinniped.dev/internal/testutil/tlsserver" ) @@ -112,10 +119,13 @@ func TestController(t *testing.T) { distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"} goodMux := http.NewServeMux() - goodOIDCIssuerServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + goodOIDCIssuerServer, goodOIDCIssuerServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) goodMux.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) + goodOIDCIssuerServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(goodOIDCIssuerServerCAPEM), + } goodMux.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -210,10 +220,13 @@ func TestController(t *testing.T) { })) badMuxInvalidJWKSURI := http.NewServeMux() - badOIDCIssuerServerInvalidJWKSURIServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badOIDCIssuerServerInvalidJWKSURIServer, badOIDCIssuerServerInvalidJWKSURIServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) badMuxInvalidJWKSURI.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) + badOIDCIssuerServerInvalidJWKSURIServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(badOIDCIssuerServerInvalidJWKSURIServerCAPEM), + } badMuxInvalidJWKSURI.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURIServer.URL, "https://.café .com/café/café/café/coffee/jwks.json") @@ -221,10 +234,13 @@ func TestController(t *testing.T) { })) badMuxInvalidJWKSURIScheme := http.NewServeMux() - badOIDCIssuerServerInvalidJWKSURISchemeServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badOIDCIssuerServerInvalidJWKSURISchemeServer, badOIDCIssuerServerInvalidJWKSURISchemeServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) badMuxInvalidJWKSURIScheme.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) + badOIDCIssuerServerInvalidJWKSURISchemeServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(badOIDCIssuerServerInvalidJWKSURISchemeServerCAPEM), + } badMuxInvalidJWKSURIScheme.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURISchemeServer.URL, "http://.café.com/café/café/café/coffee/jwks.json") @@ -232,10 +248,13 @@ func TestController(t *testing.T) { })) jwksFetchShouldFailMux := http.NewServeMux() - jwksFetchShouldFailServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jwksFetchShouldFailServer, jwksFetchShouldFailServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) jwksFetchShouldFailMux.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) + jwksFetchShouldFailServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(jwksFetchShouldFailServerCAPEM), + } jwksFetchShouldFailMux.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, jwksFetchShouldFailServer.URL, jwksFetchShouldFailServer.URL+"/fetch/will/fail/jwks.json") @@ -246,10 +265,13 @@ func TestController(t *testing.T) { // in real life. Simulating this here just so we can have test coverage for the expected error that the production // code should return in this case. badMuxUsesOurHardcodedPrivateKey := http.NewServeMux() - badOIDCIssuerUsesOurHardcodedPrivateKeyServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badOIDCIssuerUsesOurHardcodedPrivateKeyServer, badOIDCIssuerUsesOurHardcodedPrivateKeyServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) badMuxUsesOurHardcodedPrivateKey.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) + badOIDCIssuerUsesOurHardcodedPrivateKeyServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(badOIDCIssuerUsesOurHardcodedPrivateKeyServerCAPEM), + } badMuxUsesOurHardcodedPrivateKey.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL, badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL+"/jwks.json") @@ -281,12 +303,53 @@ func TestController(t *testing.T) { someJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, + } + someJWTAuthenticatorSpecWithCAInSecret := &authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: goodIssuer, + Audience: goodAudience, + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "secret-with-ca", + Key: "ca.crt", + }, + }, + } + someSecretWithCA := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret-with-ca", + Namespace: "concierge", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": goodOIDCIssuerServerCAPEM, + }, + } + someJWTAuthenticatorSpecWithCAInConfigMap := &authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: goodIssuer, + Audience: goodAudience, + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: "configmap-with-ca", + Key: "ca.crt", + }, + }, + } + someConfigMapWithCA := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap-with-ca", + Namespace: "concierge", + }, + Data: map[string]string{ + "ca.crt": string(goodOIDCIssuerServerCAPEM), + }, } someJWTAuthenticatorSpecWithUsernameClaim := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, Claims: authenticationv1alpha1.JWTTokenClaims{ Username: "my-custom-username-claim", }, @@ -294,7 +357,7 @@ func TestController(t *testing.T) { someJWTAuthenticatorSpecWithGroupsClaim := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, Claims: authenticationv1alpha1.JWTTokenClaims{ Groups: customGroupsClaim, }, @@ -319,12 +382,12 @@ func TestController(t *testing.T) { invalidIssuerJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://.café .com/café/café/café/coffee", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, } invalidIssuerSchemeJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "http://.café.com/café/café/café/coffee", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, } validIssuerURLButDoesNotExistJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer + "/foo/bar/baz/shizzle", @@ -333,22 +396,22 @@ func TestController(t *testing.T) { badIssuerJWKSURIJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: badOIDCIssuerServerInvalidJWKSURIServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIServer.TLS), + TLS: badOIDCIssuerServerInvalidJWKSURIServerTLSSpec, } badIssuerJWKSURISchemeJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: badOIDCIssuerServerInvalidJWKSURISchemeServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURISchemeServer.TLS), + TLS: badOIDCIssuerServerInvalidJWKSURISchemeServerTLSSpec, } badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerUsesOurHardcodedPrivateKeyServer.TLS), + TLS: badOIDCIssuerUsesOurHardcodedPrivateKeyServerTLSSpec, } jwksFetchShouldFailJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: jwksFetchShouldFailServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(jwksFetchShouldFailServer.TLS), + TLS: jwksFetchShouldFailServerTLSSpec, } happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { @@ -379,7 +442,7 @@ func TestController(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", - Message: "successfully parsed specified CA bundle", + Message: "spec.tls is valid: using configured CA bundle", } } happyTLSConfigurationValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition { @@ -389,7 +452,7 @@ func TestController(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", - Message: "no CA bundle specified", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", } } sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { @@ -398,8 +461,8 @@ func TestController(t *testing.T) { Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, - Reason: "InvalidTLSConfiguration", - Message: "invalid TLS configuration: illegal base64 data at input byte 7", + Reason: "InvalidTLSConfig", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7", } } @@ -640,10 +703,10 @@ func TestController(t *testing.T) { Kind: "JWTAuthenticator", } tests := []struct { - name string - cache func(*testing.T, *authncache.Cache, bool) - syncKey controllerlib.Key - jwtAuthenticators []runtime.Object + name string + cache func(*testing.T, *authncache.Cache, bool) + jwtAuthenticators []runtime.Object + secretsAndConfigMaps []runtime.Object // for modifying the clients to hack in arbitrary api responses configClient func(*conciergefake.Clientset) wantClose bool @@ -651,24 +714,18 @@ func TestController(t *testing.T) { // Errors such as url.Parse of the issuer are not returned as they imply a user error. // Since these errors trigger a resync, we are careful only to return an error when // something can be automatically corrected on a retry (ie an error that might be networking). - wantSyncLoopErr testutil.RequireErrorStringFunc - wantLogs []map[string]any - wantActions func() []coretesting.Action - wantCacheEntries int - wantUsernameClaim string - wantGroupsClaim string - runTestsOnResultingAuthenticator bool + wantSyncErr testutil.RequireErrorStringFunc + wantLogLines []string + wantActions func() []coretesting.Action + wantUsernameClaim string + wantGroupsClaim string + wantNamesOfJWTAuthenticatorsInCache []string + skipTestingCachedAuthenticator bool }{ { - name: "404: JWTAuthenticator not found will abort sync loop, no status conditions.", - syncKey: controllerlib.Key{Name: "test-name"}, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "Sync() found that the JWTAuthenticator does not exist yet or was deleted", - }, + name: "Sync: no JWTAuthenticators found results in no errors and no status conditions", + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).Sync","message":"No JWTAuthenticators found"}`, }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -678,8 +735,7 @@ func TestController(t *testing.T) { }, }, { - name: "Sync: valid and unchanged JWTAuthenticator: loop will preserve existing status conditions", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: valid and unchanged JWTAuthenticator: loop will preserve existing status conditions", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -692,27 +748,134 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"choosing to not update the jwtauthenticator status since there is no update to make","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { return []coretesting.Action{ coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: changed JWTAuthenticator: loop will update timestamps only on relevant statuses", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: multiple valid and multiple invalid JWTAuthenticators", + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-jwt-authenticator", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }, + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-jwt-authenticator", + }, + Spec: *someJWTAuthenticatorSpec, + }, + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-jwt-authenticator", + }, + Spec: *badIssuerJWKSURIJWTAuthenticatorSpec, + }, + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-invalid-jwt-authenticator", + }, + Spec: *badIssuerJWKSURIJWTAuthenticatorSpec, + }, + }, + wantSyncErr: testutil.WantExactErrorString("[" + + `error for JWTAuthenticator another-invalid-jwt-authenticator: could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name` + + ", " + + `error for JWTAuthenticator invalid-jwt-authenticator: could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name` + + "]", + ), + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"another-invalid-jwt-authenticator","issuer":"%s","removedFromCache":false}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"another-invalid-jwt-authenticator","issuer":"%s","phase":"Error"}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"choosing to not update the jwtauthenticator status since there is no update to make","jwtAuthenticator":"existing-jwt-authenticator","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"existing-jwt-authenticator","issuer":"%s","isOverwrite":false}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"invalid-jwt-authenticator","issuer":"%s","removedFromCache":false}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"invalid-jwt-authenticator","issuer":"%s","phase":"Error"}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"new-jwt-authenticator","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"new-jwt-authenticator","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, + wantActions: func() []coretesting.Action { + updateValidStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-jwt-authenticator", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateValidStatusAction.Subresource = "status" + updateInvalidStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-jwt-authenticator", + }, + Spec: *badIssuerJWKSURIJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + []metav1.Condition{ + happyIssuerURLValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadJWKSURLValidParseURI("https://.café .com/café/café/café/coffee/jwks.json", frozenMetav1Now, 0), + unknownJWKSFetch(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateInvalidStatusAction.Subresource = "status" + updateValidStatusAction.Subresource = "status" + updateAnotherInvalidStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-invalid-jwt-authenticator", + }, + Spec: *badIssuerJWKSURIJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + []metav1.Condition{ + happyIssuerURLValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadJWKSURLValidParseURI("https://.café .com/café/café/café/coffee/jwks.json", frozenMetav1Now, 0), + unknownJWKSFetch(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateAnotherInvalidStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateAnotherInvalidStatusAction, + updateInvalidStatusAction, + updateValidStatusAction, + } + }, + wantNamesOfJWTAuthenticatorsInCache: []string{ + "existing-jwt-authenticator", + "new-jwt-authenticator", + }, + }, + { + name: "Sync: changed JWTAuthenticator: loop will update timestamps only on relevant statuses", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -724,7 +887,7 @@ func TestController(t *testing.T) { Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 1233), []metav1.Condition{ - // sad and unknwn will update with new statuses and timestamps + // sad and unknown will update with new statuses and timestamps sadReadyCondition(frozenTimeInThePast, 1232), sadDiscoveryURLValidx509(goodIssuer, frozenTimeInThePast, 1231), unknownAuthenticatorValid(frozenTimeInThePast, 1232), @@ -738,16 +901,10 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -773,11 +930,10 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: valid JWTAuthenticator with CA: loop will complete successfully and update status conditions.", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: valid JWTAuthenticator with CA: loop will complete successfully and update status conditions", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -786,16 +942,10 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpec, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -814,12 +964,84 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, - runTestsOnResultingAuthenticator: true, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: JWTAuthenticator with custom username claim: loop will complete successfully and update status conditions.", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: valid JWTAuthenticator with CA from Secret: loop will complete successfully and update status conditions", + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInSecret, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someSecretWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInSecret, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: valid JWTAuthenticator with CA from ConfigMap: loop will complete successfully and update status conditions", + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInConfigMap, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someConfigMapWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInConfigMap, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: JWTAuthenticator with custom username claim: loop will complete successfully and update status conditions", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -828,16 +1050,10 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpecWithUsernameClaim, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -856,13 +1072,11 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, - wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username, - runTestsOnResultingAuthenticator: true, + wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: JWTAuthenticator with custom groups claim: loop will complete successfully and update status conditions.", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: JWTAuthenticator with custom groups claim: loop will complete successfully and update status conditions", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -871,16 +1085,10 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpecWithGroupsClaim, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -899,24 +1107,23 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, - wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups, - runTestsOnResultingAuthenticator: true, + wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: JWTAuthenticator with new fields: loop will close previous instance of JWTAuthenticator and complete successfully and update status conditions.", + name: "Sync: JWTAuthenticator with new spec fields: loop will close previous instance of JWTAuthenticator and complete successfully and update status conditions", cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + oldCA, err := base64.StdEncoding.DecodeString(otherJWTAuthenticatorSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) cache.Store( authncache.Key{ Name: "test-name", Kind: "JWTAuthenticator", APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, }, - newCacheValue(t, *otherJWTAuthenticatorSpec, wantClose), + newCacheValue(t, *otherJWTAuthenticatorSpec, string(oldCA), wantClose), ) }, - wantClose: true, - syncKey: controllerlib.Key{Name: "test-name"}, jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -925,16 +1132,10 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpec, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":true}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -953,23 +1154,32 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, - runTestsOnResultingAuthenticator: true, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, + wantClose: true, }, { - name: "Sync: JWTAuthenticator with no change: loop will abort early and not update status conditions.", + name: "Sync: previously cached authenticator gets new spec fields, but status update fails: loop will leave it in the cache and avoid calling close", cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + oldCA, err := base64.StdEncoding.DecodeString(otherJWTAuthenticatorSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) cache.Store( authncache.Key{ Name: "test-name", Kind: "JWTAuthenticator", APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, }, - newCacheValue(t, *someJWTAuthenticatorSpec, wantClose), + newCacheValue(t, *otherJWTAuthenticatorSpec, string(oldCA), wantClose), + ) + }, + configClient: func(client *conciergefake.Clientset) { + client.PrependReactor( + "update", + "jwtauthenticators", + func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("some update error") + }, ) }, - wantClose: false, - syncKey: controllerlib.Key{Name: "test-name"}, jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -978,28 +1188,189 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpec, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "actual jwt authenticator and desired jwt authenticator are the same", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", + wantLogLines: nil, // wants no logs + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + // skip the tests because the authenticator preloaded into the cache is the mock version that was added above + skipTestingCachedAuthenticator: true, + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: some update error"), + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, // keeps the old entry in the cache + wantClose: false, + }, + { + name: "Sync: previously cached valid authenticator with unchanged issuer URL and CA bundle hash has invalid status conditions in informer cache, as can happen on subsequent sync soon after multiple quick status updates (when the informer cache finally catches up): should update status in current sync", + cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + oldCA, err := base64.StdEncoding.DecodeString(someJWTAuthenticatorSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "JWTAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, *someJWTAuthenticatorSpec, string(oldCA), wantClose), + ) + }, + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + []metav1.Condition{ + sadReadyCondition(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadJWKSFetch("some remote jwks error", frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, }, - }}, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"cached jwt authenticator and desired jwt authenticator are the same: already cached, so skipping validations","jwtAuthenticator":"test-name","issuer":"%s"}`, someJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, someJWTAuthenticatorSpec.Issuer), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ // updates the status to ready + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + // skip the tests because the authenticator preloaded into the cache is the mock version that was added above + skipTestingCachedAuthenticator: true, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, // keeps the old entry in the cache + wantClose: false, + }, + { + name: "Sync: JWTAuthenticator with external and changed CA bundle: loop will close previous instance of JWTAuthenticator and complete successfully and update status conditions", + cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "JWTAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, *someJWTAuthenticatorSpecWithCAInSecret, "some-stale-ca-bundle-pem-content-from-secret", wantClose), + ) + }, + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInSecret, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someSecretWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":true}`, goodIssuer), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpecWithCAInSecret, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantClose: true, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: previously cached JWTAuthenticator with no change: will not update status conditions", + cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + oldCA, err := base64.StdEncoding.DecodeString(someJWTAuthenticatorSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "JWTAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, *someJWTAuthenticatorSpec, string(oldCA), wantClose), + ) + }, + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *someJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + Phase: "Ready", + }, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"cached jwt authenticator and desired jwt authenticator are the same: already cached, so skipping validations","jwtAuthenticator":"test-name","issuer":"%s"}`, goodIssuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"choosing to not update the jwtauthenticator status since there is no update to make","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + }, wantActions: func() []coretesting.Action { return []coretesting.Action{ coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, - runTestsOnResultingAuthenticator: false, // skip the tests because the authenticator left in the cache is the mock version that was added above + // skip the tests because the authenticator preloaded into the cache is the mock version that was added above + skipTestingCachedAuthenticator: true, + wantClose: false, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: authenticator update when cached authenticator is the wrong data type, which should never really happen: loop will complete successfully and update status conditions.", + name: "Sync: authenticator update when cached authenticator is the wrong data type, which should never really happen: loop will complete successfully and update status conditions", cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + ctrl := gomock.NewController(t) + t.Cleanup(func() { + ctrl.Finish() + }) + mockCacheValue := mockcachevalue.NewMockValue(ctrl) + mockCacheValue.EXPECT().Close().Times(1) cache.Store( authncache.Key{ Name: "test-name", @@ -1009,10 +1380,9 @@ func TestController(t *testing.T) { // Only entries of type cachedJWTAuthenticator are ever put into the cache, so this should never really happen. // This test is to provide coverage on the production code which reads from the cache and casts those entries to // the appropriate data type. - struct{ authenticator.Token }{}, + mockCacheValue, ) }, - syncKey: controllerlib.Key{Name: "test-name"}, jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1021,24 +1391,10 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpec, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "wrong JWT authenticator type in cache", - "actualType": "struct { authenticator.Token }", - }, - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).cacheValueAsJWTAuthenticator","message":"wrong JWT authenticator type in cache","jwtAuthenticator":"test-name","issuer":"%s","actualType":"*mockcachevalue.MockValue"}`, goodIssuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ @@ -1058,12 +1414,10 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, - runTestsOnResultingAuthenticator: true, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: valid JWTAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "Sync: valid JWTAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1072,6 +1426,10 @@ func TestController(t *testing.T) { Spec: *missingTLSJWTAuthenticatorSpec, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, missingTLSJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, missingTLSJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1102,12 +1460,10 @@ func TestController(t *testing.T) { }, // no explicit logs, this is an issue of config, the user must provide TLS config for the // custom cert provided for this server. - wantSyncLoopErr: testutil.WantSprintfErrorString(`could not perform oidc discovery on provider issuer: Get "%s/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`, goodIssuer), - wantCacheEntries: 0, + wantSyncErr: testutil.WantSprintfErrorString(`error for JWTAuthenticator test-name: could not perform oidc discovery on provider issuer: Get "%s/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`, goodIssuer), }, { - name: "validateTLS: JWTAuthenticator with invalid CA: loop will fail, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "validateTLS: JWTAuthenticator with invalid CA: loop will fail, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1116,6 +1472,10 @@ func TestController(t *testing.T) { Spec: *invalidTLSJWTAuthenticatorSpec, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, invalidTLSJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, invalidTLSJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1144,10 +1504,9 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, }, { - name: "previously valid cached authenticator's spec changes and becomes invalid (e.g. spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache", + name: "previously valid cached authenticator (which did not specify a CA bundle) changes and becomes invalid due to any problem with the CA bundle: loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache and close it", cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { cache.Store( authncache.Key{ @@ -1155,7 +1514,67 @@ func TestController(t *testing.T) { Kind: "JWTAuthenticator", APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, }, - newCacheValue(t, *someJWTAuthenticatorSpec, wantClose), + // Force an invalid spec into the cache, which is not very realistic, but it simulates a case + // where the CA bundle goes from being cached as empty to being an error during validation, + // without causing any changes in the spec. This test wants to prove that the rest of the + // validations get run and the resource is update, just in case that can happen somehow. + newCacheValue(t, *invalidTLSJWTAuthenticatorSpec, "", wantClose), + ) + }, + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *invalidTLSJWTAuthenticatorSpec, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":true}`, invalidTLSJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, invalidTLSJWTAuthenticatorSpec.Issuer), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *invalidTLSJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(someOtherIssuer, frozenMetav1Now, 0), + []metav1.Condition{ + sadReadyCondition(frozenMetav1Now, 0), + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownDiscoveryURLValid(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + unknownJWKSURLValid(frozenMetav1Now, 0), + unknownJWKSFetch(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantClose: true, + }, + { + name: "previously valid cached authenticator's spec changes and becomes invalid for any other reason (this test uses an invalid spec.issuer URL): loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache", + cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) { + oldCA, err := base64.StdEncoding.DecodeString(someJWTAuthenticatorSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "JWTAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, *someJWTAuthenticatorSpec, string(oldCA), wantClose), ) }, jwtAuthenticators: []runtime.Object{ @@ -1166,7 +1585,10 @@ func TestController(t *testing.T) { Spec: *invalidIssuerJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":true}`, invalidIssuerJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, invalidIssuerJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1195,8 +1617,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, // removed from cache - wantClose: true, // the removed cache entry was also closed + wantNamesOfJWTAuthenticatorsInCache: []string{}, // it was removed from the cache + wantClose: true, // the removed cache entry was also closed }, { name: "validateIssuer: parsing error (spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", @@ -1208,7 +1630,10 @@ func TestController(t *testing.T) { Spec: *invalidIssuerJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, invalidIssuerJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, invalidIssuerJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1248,7 +1673,10 @@ func TestController(t *testing.T) { Spec: *invalidIssuerSchemeJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, invalidIssuerSchemeJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, invalidIssuerSchemeJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1288,11 +1716,14 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/#do-not-include-fragment", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/#do-not-include-fragment","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/#do-not-include-fragment","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1301,7 +1732,7 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/#do-not-include-fragment", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, Status: authenticationv1alpha1.JWTAuthenticatorStatus{ Conditions: conditionstestutil.Replace( @@ -1336,11 +1767,14 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/?query-params=not-allowed", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/?query-params=not-allowed","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/?query-params=not-allowed","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1349,7 +1783,7 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/?query-params=not-allowed", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, Status: authenticationv1alpha1.JWTAuthenticatorStatus{ Conditions: conditionstestutil.Replace( @@ -1384,11 +1818,14 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/.well-known/openid-configuration", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/.well-known/openid-configuration","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"https://www.example.com/foo/bar/.well-known/openid-configuration","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1397,7 +1834,7 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://www.example.com/foo/bar/.well-known/openid-configuration", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, Status: authenticationv1alpha1.JWTAuthenticatorStatus{ Conditions: conditionstestutil.Replace( @@ -1432,7 +1869,10 @@ func TestController(t *testing.T) { Spec: *validIssuerURLButDoesNotExistJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, validIssuerURLButDoesNotExistJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, validIssuerURLButDoesNotExistJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1462,7 +1902,7 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`could not perform oidc discovery on provider issuer: Get "` + goodIssuer + `/foo/bar/baz/shizzle/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`), + wantSyncErr: testutil.WantExactErrorString(`error for JWTAuthenticator test-name: could not perform oidc discovery on provider issuer: Get "` + goodIssuer + `/foo/bar/baz/shizzle/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`), }, { name: "validateProviderDiscovery: excessively long errors truncated: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", @@ -1474,11 +1914,14 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer + "/path/to/not/found", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, goodIssuer+"/path/to/not/found"), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, goodIssuer+"/path/to/not/found"), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1487,7 +1930,7 @@ func TestController(t *testing.T) { Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer + "/path/to/not/found", Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), + TLS: goodOIDCIssuerServerTLSSpec, }, Status: authenticationv1alpha1.JWTAuthenticatorStatus{ Conditions: conditionstestutil.Replace( @@ -1513,7 +1956,7 @@ func TestController(t *testing.T) { } }, // not currently truncating the logged err - wantSyncLoopErr: testutil.WantExactErrorString("could not perform oidc discovery on provider issuer: 404 Not Found: \n\t\t \t404 not found page\n\t\t\tlots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz should not reach end of string\n\t\t"), + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: could not perform oidc discovery on provider issuer: 404 Not Found: \n\t\t \t404 not found page\n\t\t\tlots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz should not reach end of string\n\t\t"), }, // cannot be tested currently the way the coreos lib works. // the constructor requires an issuer in the payload and validates the issuer matches the actual issuer, @@ -1529,7 +1972,10 @@ func TestController(t *testing.T) { Spec: *badIssuerJWKSURIJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, badIssuerJWKSURIJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1557,7 +2003,7 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name`), + wantSyncErr: testutil.WantExactErrorString(`error for JWTAuthenticator test-name: could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name`), }, { name: "validateProviderJWKSURL: invalid scheme, requires 'https': loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", @@ -1569,7 +2015,10 @@ func TestController(t *testing.T) { Spec: *badIssuerJWKSURISchemeJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, badIssuerJWKSURISchemeJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, badIssuerJWKSURISchemeJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1597,7 +2046,7 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString("jwks_uri http://.café.com/café/café/café/coffee/jwks.json has invalid scheme, require 'https'"), + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: jwks_uri http://.café.com/café/café/café/coffee/jwks.json has invalid scheme, require 'https'"), }, { name: "validateProviderJWKSURL: remote jwks should not have been able to verify hardcoded test jwt token: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", @@ -1609,7 +2058,10 @@ func TestController(t *testing.T) { Spec: *badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1638,7 +2090,7 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString("remote jwks should not have been able to verify hardcoded test jwt token"), + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: remote jwks should not have been able to verify hardcoded test jwt token"), }, { name: "validateJWKSFetch: could not fetch keys: loop will fail sync, will write failed and unknown status conditions, and will enqueue a resync", @@ -1650,7 +2102,10 @@ func TestController(t *testing.T) { Spec: *jwksFetchShouldFailJWTAuthenticatorSpec, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"invalid jwt authenticator","jwtAuthenticator":"test-name","issuer":"%s","removedFromCache":false}`, jwksFetchShouldFailJWTAuthenticatorSpec.Issuer), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Error"}`, jwksFetchShouldFailJWTAuthenticatorSpec.Issuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1678,7 +2133,7 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString("could not fetch keys: fetching keys oidc: get keys failed: 404 Not Found 404 page not found\n"), + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: could not fetch keys: fetching keys oidc: get keys failed: 404 Not Found 404 page not found\n"), }, { name: "updateStatus: called with matching original and updated conditions: will not make request to update conditions", @@ -1694,24 +2149,17 @@ func TestController(t *testing.T) { }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"choosing to not update the jwtauthenticator status since there is no update to make","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { return []coretesting.Action{ coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { name: "updateStatus: called with different original and updated conditions: will make request to update conditions", @@ -1732,17 +2180,10 @@ func TestController(t *testing.T) { }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).updateStatus","message":"jwtauthenticator status successfully updated","jwtAuthenticator":"test-name","issuer":"%s","phase":"Ready"}`, goodIssuer), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"jwtcachefiller-controller","caller":"jwtcachefiller/jwtcachefiller.go:$jwtcachefiller.(*jwtCacheFillerController).syncIndividualJWTAuthenticator","message":"added or updated jwt authenticator in cache","jwtAuthenticator":"test-name","issuer":"%s","isOverwrite":false}`, goodIssuer), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1761,28 +2202,18 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, + wantNamesOfJWTAuthenticatorsInCache: []string{"test-name"}, }, { - name: "updateStatus: when update request fails: error will enqueue a resync", + name: "updateStatus: given a valid JWTAuthenticator spec, when update request fails: error will enqueue a resync and the authenticator will not be added to the cache", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", }, Spec: *someJWTAuthenticatorSpec, - Status: authenticationv1alpha1.JWTAuthenticatorStatus{ - Conditions: conditionstestutil.Replace( - allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), - []metav1.Condition{ - sadReadyCondition(frozenMetav1Now, 0), - }, - ), - Phase: "SomethingThatWontUpdate", - }, }, }, - syncKey: controllerlib.Key{Name: "test-name"}, configClient: func(client *conciergefake.Clientset) { client.PrependReactor( "update", @@ -1794,7 +2225,7 @@ func TestController(t *testing.T) { }, wantActions: func() []coretesting.Action { // This captures that there was an attempt to update to Ready, allHappyConditions, - // but the wantSyncLoopErr indicates that there is a failure, so the JWTAuthenticator + // but the wantSyncErr indicates that there is a failure, so the JWTAuthenticator // remains with a bad phase and at least 1 sad condition updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1813,18 +2244,9 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", - }, - }}, - wantSyncLoopErr: testutil.WantExactErrorString("some update error"), - wantCacheEntries: 1, + wantLogLines: nil, // wants no logs + wantSyncErr: testutil.WantExactErrorString("error for JWTAuthenticator test-name: some update error"), + wantNamesOfJWTAuthenticatorsInCache: []string{}, // even though the authenticator was valid, do not cache it because the status update failed }, // cannot be tested the way we are invoking oidc.New as we don't provide enough configuration // knobs to actually invoke the code in a broken way. We always give a good client, good keys, and @@ -1842,6 +2264,7 @@ func TestController(t *testing.T) { tt.configClient(pinnipedAPIClient) } pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + kubeInformers := kubeinformers.NewSharedInformerFactory(kubernetesfake.NewSimpleClientset(tt.secretsAndConfigMaps...), 0) cache := authncache.New() var log bytes.Buffer @@ -1852,9 +2275,13 @@ func TestController(t *testing.T) { } controller := New( + "concierge", // namespace for the controller cache, pinnipedAPIClient, pinnipedInformers.Authentication().V1alpha1().JWTAuthenticators(), + kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), + controllerlib.WithInformer, frozenClock, logger) @@ -1862,42 +2289,17 @@ func TestController(t *testing.T) { defer cancel() pinnipedInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) - syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey} + syncCtx := controllerlib.Context{Context: ctx} - if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncLoopErr != nil { - testutil.RequireErrorStringFromErr(t, err, tt.wantSyncLoopErr) + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncErr != nil { + testutil.RequireErrorStringFromErr(t, err, tt.wantSyncErr) } else { require.NoError(t, err) } - actualLogLines := testutil.SplitByNewline(log.String()) - require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct") - - for logLineNum, logLine := range actualLogLines { - require.NotNil(t, tt.wantLogs[logLineNum], "expected log line should never be empty") - var lineStruct map[string]any - err := json.Unmarshal([]byte(logLine), &lineStruct) - require.NoError(t, err) - - require.Equal(t, tt.wantLogs[logLineNum]["level"], lineStruct["level"], fmt.Sprintf("log line (%d) log level should be correct (in: %s)", logLineNum, lineStruct)) - - require.Equal(t, tt.wantLogs[logLineNum]["timestamp"], lineStruct["timestamp"], fmt.Sprintf("log line (%d) timestamp should be correct (in: %s)", logLineNum, lineStruct)) - require.Equal(t, tt.wantLogs[logLineNum]["logger"], lineStruct["logger"], fmt.Sprintf("log line (%d) logger should be correct", logLineNum)) - require.NotEmpty(t, lineStruct["caller"], fmt.Sprintf("log line (%d) caller should not be empty", logLineNum)) - require.Equal(t, tt.wantLogs[logLineNum]["message"], lineStruct["message"], fmt.Sprintf("log line (%d) message should be correct", logLineNum)) - if lineStruct["issuer"] != nil { - require.Equal(t, tt.wantLogs[logLineNum]["issuer"], lineStruct["issuer"], fmt.Sprintf("log line (%d) issuer should be correct", logLineNum)) - } - if lineStruct["jwtAuthenticator"] != nil { - require.Equal(t, tt.wantLogs[logLineNum]["jwtAuthenticator"], lineStruct["jwtAuthenticator"], fmt.Sprintf("log line (%d) jwtAuthenticator should be correct", logLineNum)) - } - if lineStruct["actualType"] != nil { - require.Equal(t, tt.wantLogs[logLineNum]["actualType"], lineStruct["actualType"], fmt.Sprintf("log line (%d) actualType should be correct", logLineNum)) - } - } - if !assert.ElementsMatch(t, tt.wantActions(), pinnipedAPIClient.Actions()) { // cmp.Diff is superior to require.ElementsMatch in terms of readability here. // require.ElementsMatch will handle pointers better than require.Equal, but @@ -1905,107 +2307,125 @@ func TestController(t *testing.T) { require.Fail(t, cmp.Diff(tt.wantActions(), pinnipedAPIClient.Actions()), "actions should be exactly the expected number of actions and also contain the correct resources") } - require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys())) + require.Equal(t, len(tt.wantNamesOfJWTAuthenticatorsInCache), len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", len(tt.wantNamesOfJWTAuthenticatorsInCache), len(cache.Keys()), cache.Keys())) - if !tt.runTestsOnResultingAuthenticator { - return // end of test unless we wanted to run tests on the resulting authenticator from the cache + actualLog, _ := strings.CutSuffix(log.String(), "\n") + actualLogLines := strings.Split(actualLog, "\n") + var jwtLogLines []string + for _, line := range actualLogLines { + if strings.Contains(line, "jwtcachefiller/jwtcachefiller.go") { + jwtLogLines = append(jwtLogLines, line) + } } - // We expected the cache to have an entry, so pull that entry from the cache and test it. - expectedCacheKey := authncache.Key{ - APIGroup: authenticationv1alpha1.GroupName, - Kind: "JWTAuthenticator", - Name: syncCtx.Key.Name, - } - cachedAuthenticator, ok := cache.Get(expectedCacheKey).(tokenAuthenticatorCloser) - require.True(t, ok) - require.NotNil(t, cachedAuthenticator) + require.Equal(t, tt.wantLogLines, jwtLogLines) - // Schedule it to be closed at the end of the test. - t.Cleanup(cachedAuthenticator.Close) + for _, name := range tt.wantNamesOfJWTAuthenticatorsInCache { + // We expected the cache to have an entry, so pull that entry from the cache and test it. + expectedCacheKey := authncache.Key{ + APIGroup: authenticationv1alpha1.GroupName, + Kind: "JWTAuthenticator", + Name: name, + } + temp := cache.Get(expectedCacheKey) + require.NotNil(t, temp) + require.IsType(t, &cachedJWTAuthenticator{}, temp) - const ( - goodSubject = "some-subject" - group0 = "some-group-0" - group1 = "some-group-1" - goodUsername = "pinny123" - ) + if tt.skipTestingCachedAuthenticator { + continue // skip the rest of this test for this authenticator + } - if tt.wantUsernameClaim == "" { - tt.wantUsernameClaim = "username" - } + require.NotNil(t, temp.(*cachedJWTAuthenticator).Token) + cachedAuthenticator, ok := temp.(tokenAuthenticatorCloser) + require.True(t, ok) - if tt.wantGroupsClaim == "" { - tt.wantGroupsClaim = "groups" - } + // Schedule it to be closed at the end of the test. + t.Cleanup(cachedAuthenticator.Close) - for _, test := range testTableForAuthenticateTokenTests( - t, - goodRSASigningKey, - goodRSASigningAlgo, - goodRSASigningKeyID, - group0, - group1, - goodUsername, - tt.wantUsernameClaim, - tt.wantGroupsClaim, - goodIssuer, - ) { - t.Run(test.name, func(t *testing.T) { - t.Parallel() + const ( + goodSubject = "some-subject" + group0 = "some-group-0" + group1 = "some-group-1" + goodUsername = "pinny123" + ) - wellKnownClaims := josejwt.Claims{ - Issuer: goodIssuer, - Subject: goodSubject, - Audience: []string{goodAudience}, - Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)), - NotBefore: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), - IssuedAt: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), - } - var groups any - username := goodUsername - if test.jwtClaims != nil { - test.jwtClaims(&wellKnownClaims, &groups, &username) - } + if tt.wantUsernameClaim == "" { + tt.wantUsernameClaim = "username" + } - var signingKey any = goodECSigningKey - signingAlgo := goodECSigningAlgo - signingKID := goodECSigningKeyID - if test.jwtSignature != nil { - test.jwtSignature(&signingKey, &signingAlgo, &signingKID) - } + if tt.wantGroupsClaim == "" { + tt.wantGroupsClaim = "groups" + } - jwt := createJWT( - t, - signingKey, - signingAlgo, - signingKID, - &wellKnownClaims, - tt.wantGroupsClaim, - groups, - test.distributedGroupsClaimURL, - tt.wantUsernameClaim, - username, - ) + for _, test := range testTableForAuthenticateTokenTests( + t, + goodRSASigningKey, + goodRSASigningAlgo, + goodRSASigningKeyID, + group0, + group1, + goodUsername, + tt.wantUsernameClaim, + tt.wantGroupsClaim, + goodIssuer, + ) { + t.Run(test.name, func(t *testing.T) { + t.Parallel() - // Loop for a while here to allow the underlying OIDC authenticator to initialize itself asynchronously. - var ( - rsp *authenticator.Response - authenticated bool - err error - ) - _ = wait.PollUntilContextTimeout(context.Background(), 10*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) { - rsp, authenticated, err = cachedAuthenticator.AuthenticateToken(context.Background(), jwt) - return !isNotInitialized(err), nil + wellKnownClaims := josejwt.Claims{ + Issuer: goodIssuer, + Subject: goodSubject, + Audience: []string{goodAudience}, + Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)), + NotBefore: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), + IssuedAt: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), + } + var groups any + username := goodUsername + if test.jwtClaims != nil { + test.jwtClaims(&wellKnownClaims, &groups, &username) + } + + var signingKey any = goodECSigningKey + signingAlgo := goodECSigningAlgo + signingKID := goodECSigningKeyID + if test.jwtSignature != nil { + test.jwtSignature(&signingKey, &signingAlgo, &signingKID) + } + + jwt := createJWT( + t, + signingKey, + signingAlgo, + signingKID, + &wellKnownClaims, + tt.wantGroupsClaim, + groups, + test.distributedGroupsClaimURL, + tt.wantUsernameClaim, + username, + ) + + // Loop for a while here to allow the underlying OIDC authenticator to initialize itself asynchronously. + var ( + rsp *authenticator.Response + authenticated bool + err error + ) + _ = wait.PollUntilContextTimeout(context.Background(), 10*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) { + require.NotEmpty(t, cachedAuthenticator) + rsp, authenticated, err = cachedAuthenticator.AuthenticateToken(context.Background(), jwt) + return !isNotInitialized(err), nil + }) + if test.wantErr != nil { + testutil.RequireErrorStringFromErr(t, err, test.wantErr) + } else { + require.NoError(t, err) + require.Equal(t, test.wantResponse, rsp) + require.Equal(t, test.wantAuthenticated, authenticated) + } }) - if test.wantErr != nil { - testutil.RequireErrorStringFromErr(t, err, test.wantErr) - } else { - require.NoError(t, err) - require.Equal(t, test.wantResponse, rsp) - require.Equal(t, test.wantAuthenticated, authenticated) - } - }) + } } }) } @@ -2140,7 +2560,7 @@ func testTableForAuthenticateTokenTests( jwtClaims: func(_ *josejwt.Claims, groups *any, username *string) { *groups = map[string]string{"not an array": "or a string"} }, - wantErr: testutil.WantMatchingErrorString("oidc: parse groups claim \"" + expectedGroupsClaim + "\": json: cannot unmarshal object into Go value of type string"), + wantErr: testutil.WantMatchingErrorString(`oidc: parse groups claim "` + expectedGroupsClaim + `": json: cannot unmarshal object into Go value of type string`), }, { name: "bad token with wrong issuer", @@ -2254,7 +2674,7 @@ func createJWT( return jwt } -func newCacheValue(t *testing.T, spec authenticationv1alpha1.JWTAuthenticatorSpec, wantClose bool) authncache.Value { +func newCacheValue(t *testing.T, spec authenticationv1alpha1.JWTAuthenticatorSpec, caBundle string, wantClose bool) authncache.Value { t.Helper() wasClosed := false @@ -2263,9 +2683,156 @@ func newCacheValue(t *testing.T, spec authenticationv1alpha1.JWTAuthenticatorSpe }) return &cachedJWTAuthenticator{ - spec: &spec, + issuer: spec.Issuer, + caBundleHash: tlsconfigutil.NewCABundleHash([]byte(caBundle)), cancel: func() { wasClosed = true }, } } + +func TestControllerFilterSecret(t *testing.T) { + tests := []struct { + name string + secret metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "should return true for a secret of the type Opaque", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return true for a secret of the type TLS", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", + secret: &corev1.Secret{ + Type: "other-type", + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + }, + { + name: "should return false for a resource of wrong data type", + secret: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClock := clocktesting.NewFakeClock(nowDoesntMatter) + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + pinnipedAPIClient := conciergefake.NewSimpleClientset() + pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + "concierge", // namespace for the controller + authncache.New(), + pinnipedAPIClient, + pinnipedInformers.Authentication().V1alpha1().JWTAuthenticators(), + secretInformer, + configMapInformer, + observableInformers.WithInformer, + frozenClock, + logger) + + unrelated := &corev1.Secret{} + filter := observableInformers.GetFilterForInformer(secretInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.secret, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.secret)) + }) + } +} + +func TestControllerFilterConfigMap(t *testing.T) { + namespace := "some-namespace" + goodCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "a configMap in the right namespace", + cm: goodCM, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClock := clocktesting.NewFakeClock(nowDoesntMatter) + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + pinnipedAPIClient := conciergefake.NewSimpleClientset() + pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + "concierge", // namespace for the controller + authncache.New(), + pinnipedAPIClient, + pinnipedInformers.Authentication().V1alpha1().JWTAuthenticators(), + secretInformer, + configMapInformer, + observableInformers.WithInformer, + frozenClock, + logger) + + unrelated := &corev1.ConfigMap{} + filter := observableInformers.GetFilterForInformer(configMapInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.cm, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.cm)) + }) + } +} diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go index f9400db4b..2361381cc 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go @@ -11,18 +11,21 @@ import ( "fmt" "net/url" "reflect" + "slices" + "strings" "time" k8sauthv1beta1 "k8s.io/api/authentication/v1beta1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" utilerrors "k8s.io/apimachinery/pkg/util/errors" k8snetutil "k8s.io/apimachinery/pkg/util/net" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" + corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/rest" - "k8s.io/klog/v2" "k8s.io/utils/clock" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" @@ -31,6 +34,7 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controller/authenticator/authncache" "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" @@ -39,34 +43,40 @@ import ( ) const ( - controllerName = "webhookcachefiller-controller" - typeReady = "Ready" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeWebhookConnectionValid = "WebhookConnectionValid" - typeEndpointURLValid = "EndpointURLValid" - typeAuthenticatorValid = "AuthenticatorValid" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" + controllerName = "webhookcachefiller-controller" + + typeReady = "Ready" + typeWebhookConnectionValid = "WebhookConnectionValid" + typeEndpointURLValid = "EndpointURLValid" + typeAuthenticatorValid = "AuthenticatorValid" + reasonUnableToCreateClient = "UnableToCreateClient" reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook" - reasonInvalidTLSConfiguration = "InvalidTLSConfiguration" reasonInvalidEndpointURL = "InvalidEndpointURL" reasonInvalidEndpointURLScheme = "InvalidEndpointURLScheme" - reasonUnableToDialServer = "UnableToDialServer" - msgUnableToValidate = "unable to validate; see other conditions for details" + + msgUnableToValidate = "unable to validate; see other conditions for details" ) type cachedWebhookAuthenticator struct { authenticator.Token - spec *authenticationv1alpha1.WebhookAuthenticatorSpec + endpoint string + caBundleHash tlsconfigutil.CABundleHash +} + +func (*cachedWebhookAuthenticator) Close() { + // no-op, because no cleanup is needed on webhook authenticators } // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache. func New( + namespace string, cache *authncache.Cache, client conciergeclientset.Interface, - webhooks authinformers.WebhookAuthenticatorInformer, + webhookInformer authinformers.WebhookAuthenticatorInformer, + secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, clock clock.Clock, log plog.Logger, ) controllerlib.Controller { @@ -74,124 +84,253 @@ func New( controllerlib.Config{ Name: controllerName, Syncer: &webhookCacheFillerController{ - cache: cache, - client: client, - webhooks: webhooks, - clock: clock, - log: log.WithName(controllerName), + namespace: namespace, + cache: cache, + client: client, + webhookInformer: webhookInformer, + secretInformer: secretInformer, + configMapInformer: configMapInformer, + clock: clock, + log: log.WithName(controllerName), }, }, - controllerlib.WithInformer( - webhooks, - pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct + withInformer( + webhookInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue(), + ), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) } type webhookCacheFillerController struct { - cache *authncache.Cache - webhooks authinformers.WebhookAuthenticatorInformer - client conciergeclientset.Interface - clock clock.Clock - log plog.Logger + namespace string + cache *authncache.Cache + webhookInformer authinformers.WebhookAuthenticatorInformer + secretInformer corev1informers.SecretInformer + configMapInformer corev1informers.ConfigMapInformer + client conciergeclientset.Interface + clock clock.Clock + log plog.Logger } // Sync implements controllerlib.Syncer. func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error { - obj, err := c.webhooks.Lister().Get(ctx.Key.Name) - if err != nil && apierrors.IsNotFound(err) { - c.log.Info("Sync() found that the WebhookAuthenticator does not exist yet or was deleted") - return nil - } + webhookAuthenticators, err := c.webhookInformer.Lister().List(labels.Everything()) if err != nil { - // no unit test for this failure - return fmt.Errorf("failed to get WebhookAuthenticator %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) + return err } + if len(webhookAuthenticators) == 0 { + c.log.Info("No WebhookAuthenticators found") + return nil + } + + // Sort them by name so that order is predictable and therefore output is consistent for tests and logs. + slices.SortStableFunc(webhookAuthenticators, func(a, b *authenticationv1alpha1.WebhookAuthenticator) int { + return strings.Compare(a.Name, b.Name) + }) + + var errs []error + for _, webhookAuthenticator := range webhookAuthenticators { + err = c.syncIndividualWebhookAuthenticator(ctx.Context, webhookAuthenticator) + if err != nil { + errs = append(errs, fmt.Errorf("error for WebhookAuthenticator %s: %w", webhookAuthenticator.Name, err)) + } + } + return utilerrors.NewAggregate(errs) +} + +func (c *webhookCacheFillerController) syncIndividualWebhookAuthenticator(ctx context.Context, webhookAuthenticator *authenticationv1alpha1.WebhookAuthenticator) error { cacheKey := authncache.Key{ APIGroup: authenticationv1alpha1.GroupName, Kind: "WebhookAuthenticator", - Name: ctx.Key.Name, + Name: webhookAuthenticator.Name, } - // Only revalidate and update the cache if the cached authenticator is different from the desired authenticator. - // There is no need to repeat validations for a spec that was already successfully validated. We are making a - // design decision to avoid repeating the validation which dials the server, even though the server's TLS - // configuration could have changed, because it is also possible that the network could be flaky. We are choosing - // to prefer to keep the authenticator cached (available for end-user auth attempts) during times of network flakes - // rather than trying to show the most up-to-date status possible. These validations are for administrator - // convenience at the time of a configuration change, to catch typos and blatant misconfigurations, rather - // than to constantly monitor for external issues. - if valueFromCache := c.cache.Get(cacheKey); valueFromCache != nil { - webhookAuthenticatorFromCache := c.cacheValueAsWebhookAuthenticator(valueFromCache) - if webhookAuthenticatorFromCache != nil && reflect.DeepEqual(webhookAuthenticatorFromCache.spec, &obj.Spec) { - c.log.WithValues("webhookAuthenticator", klog.KObj(obj), "endpoint", obj.Spec.Endpoint). - Info("actual webhook authenticator and desired webhook authenticator are the same") - // Stop, no more work to be done. This authenticator is already validated and cached. - return nil - } - } + logger := c.log.WithValues( + "webhookAuthenticator", webhookAuthenticator.Name, + "endpoint", webhookAuthenticator.Spec.Endpoint) - conditions := make([]*metav1.Condition, 0) var errs []error + conditions := make([]*metav1.Condition, 0) + var newWebhookAuthenticatorForCache *cachedWebhookAuthenticator - certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(obj.Spec.TLS, conditions) - endpointHostPort, conditions, endpointOk := c.validateEndpoint(obj.Spec.Endpoint, conditions) + caBundle, conditions, tlsBundleOk := c.validateTLSBundle(webhookAuthenticator.Spec.TLS, conditions) + + endpointHostPort, conditions, endpointOk := c.validateEndpoint(webhookAuthenticator.Spec.Endpoint, conditions) okSoFar := tlsBundleOk && endpointOk - conditions, tlsNegotiateErr := c.validateConnection(certPool, endpointHostPort, conditions, okSoFar) + // Only revalidate and update the cache if the cached authenticator is different from the desired authenticator. + // There is no need to repeat connection probe validations for a URL and CA bundle combination that was already + // successfully validated. We are making a design decision to avoid repeating the validation which dials the server, + // even though the server's TLS configuration could have changed, because it is also possible that the network + // could be flaky. We are choosing to prefer to keep the authenticator cached (available for end-user auth attempts) + // during times of network flakes rather than trying to show the most up-to-date status possible. These validations + // are for administrator convenience at the time of a configuration change, to catch typos and blatant + // misconfigurations, rather than to constantly monitor for external issues. + foundAuthenticatorInCache, previouslyValidatedWithSameEndpointAndBundle := c.havePreviouslyValidated( + cacheKey, webhookAuthenticator.Spec.Endpoint, tlsBundleOk, caBundle.Hash(), logger) + if previouslyValidatedWithSameEndpointAndBundle { + // Because the authenticator was previously cached, that implies that the following conditions were + // previously validated. These are the expensive validations to repeat, so skip them this time. + // However, the status may be lagging behind due to the informer cache being slow to catch up + // after previous status updates, so always calculate the new status conditions again and check + // if they need to be updated. + logger.Info("cached webhook authenticator and desired webhook authenticator are the same: already cached, so skipping validations") + conditions = append(conditions, + successfulWebhookConnectionValidCondition(), + successfulAuthenticatorValidCondition(), + ) + } else { + // Run all remaining validations. + a, moreConditions, moreErrs := c.doExpensiveValidations(webhookAuthenticator, endpointHostPort, caBundle, okSoFar, logger) + newWebhookAuthenticatorForCache = a + conditions = append(conditions, moreConditions...) + errs = append(errs, moreErrs...) + } + + authenticatorValid := !conditionsutil.HadErrorCondition(conditions) + + // If we calculated a failed status condition, then remove it from the cache even before we try to write + // the status, because writing the status can fail for various reasons. + if !authenticatorValid { + // The authenticator was determined to be invalid. Remove it from the cache, in case it was previously + // validated and cached. Do not allow an old, previously validated spec of the authenticator to continue + // being used for authentication. + c.cache.Delete(cacheKey) + logger.Info("invalid webhook authenticator", + "removedFromCache", foundAuthenticatorInCache) + } + + // Always try to update the status, even when we found it in the authenticator cache. + updateErr := c.updateStatus(ctx, webhookAuthenticator, conditions, logger) + errs = append(errs, updateErr) + + // Only add/update this authenticator to the cache when we have a new one and the status update succeeded. + if newWebhookAuthenticatorForCache != nil && authenticatorValid && updateErr == nil { + c.cache.Store(cacheKey, newWebhookAuthenticatorForCache) + logger.Info("added or updated webhook authenticator in cache", + "isOverwrite", foundAuthenticatorInCache) + } + + // 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 utilerrors.NewAggregate(errs) +} + +func (c *webhookCacheFillerController) doExpensiveValidations( + webhookAuthenticator *authenticationv1alpha1.WebhookAuthenticator, + endpointHostPort *endpointaddr.HostPort, + caBundle *tlsconfigutil.CABundle, + okSoFar bool, + logger plog.Logger, +) (*cachedWebhookAuthenticator, []*metav1.Condition, []error) { + var newWebhookAuthenticatorForCache *cachedWebhookAuthenticator + var conditions []*metav1.Condition + var errs []error + + conditions, tlsNegotiateErr := c.validateConnection(caBundle.CertPool(), endpointHostPort, conditions, okSoFar, logger) errs = append(errs, tlsNegotiateErr) okSoFar = okSoFar && tlsNegotiateErr == nil - newWebhookAuthenticatorForCache, conditions, err := newWebhookAuthenticator( + newAuthenticator, conditions, err := newWebhookAuthenticator( // Note that we use the whole URL when constructing the webhook client, // not just the host and port that we validated above. We need the path, etc. - obj.Spec.Endpoint, - pemBytes, + webhookAuthenticator.Spec.Endpoint, + caBundle.PEMBytes(), conditions, okSoFar, ) errs = append(errs, err) - if conditionsutil.HadErrorCondition(conditions) { - // The authenticator was determined to be invalid. Remove it from the cache, in case it was previously - // validated and cached. Do not allow an old, previously validated spec of the authenticator to continue - // being used for authentication. - c.cache.Delete(cacheKey) - } else { - c.cache.Store(cacheKey, &cachedWebhookAuthenticator{ - Token: newWebhookAuthenticatorForCache, - spec: obj.Spec.DeepCopy(), // deep copy to avoid caching original object - }) - c.log.WithValues("webhook", klog.KObj(obj), "endpoint", obj.Spec.Endpoint). - Info("added new webhook authenticator") + if newAuthenticator != nil { + newWebhookAuthenticatorForCache = &cachedWebhookAuthenticator{ + Token: newAuthenticator, + endpoint: webhookAuthenticator.Spec.Endpoint, + caBundleHash: caBundle.Hash(), + } } - - 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 utilerrors.NewAggregate(errs) + return newWebhookAuthenticatorForCache, conditions, errs } -func (c *webhookCacheFillerController) cacheValueAsWebhookAuthenticator(value authncache.Value) *cachedWebhookAuthenticator { +func (c *webhookCacheFillerController) havePreviouslyValidated( + cacheKey authncache.Key, + endpoint string, + tlsBundleOk bool, + caBundleHash tlsconfigutil.CABundleHash, + logger plog.Logger, +) (bool, bool) { + var authenticatorFromCache *cachedWebhookAuthenticator + valueFromCache := c.cache.Get(cacheKey) + if valueFromCache == nil { + return false, false + } + authenticatorFromCache = c.cacheValueAsWebhookAuthenticator(valueFromCache, logger) + if authenticatorFromCache == nil { + return false, false + } + if authenticatorFromCache.endpoint == endpoint && + tlsBundleOk && // if there was any error while validating the latest CA bundle, then do not consider it previously validated + authenticatorFromCache.caBundleHash.Equal(caBundleHash) { + return true, true + } + return true, false // found the authenticator, but it had not been previously validated with these same settings +} + +func (c *webhookCacheFillerController) cacheValueAsWebhookAuthenticator(value authncache.Value, log plog.Logger) *cachedWebhookAuthenticator { webhookAuthenticator, ok := value.(*cachedWebhookAuthenticator) if !ok { actualType := "" if t := reflect.TypeOf(value); t != nil { actualType = t.String() } - c.log.WithValues("actualType", actualType).Info("wrong webhook authenticator type in cache") + log.Info("wrong webhook authenticator type in cache", + "actualType", actualType) return nil } return webhookAuthenticator } +func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*tlsconfigutil.CABundle, []*metav1.Condition, bool) { + condition, caBundle := tlsconfigutil.ValidateTLSConfig( + tlsconfigutil.TLSSpecForConcierge(tlsSpec), + "spec.tls", + c.namespace, + c.secretInformer, + c.configMapInformer) + + conditions = append(conditions, condition) + return caBundle, conditions, condition.Status == metav1.ConditionTrue +} + +func successfulAuthenticatorValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeAuthenticatorValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "authenticator initialized", + } +} + // newWebhookAuthenticator creates a webhook from the provided API server url and caBundle // used to validate TLS connections. func newWebhookAuthenticator( @@ -204,7 +343,7 @@ func newWebhookAuthenticator( conditions = append(conditions, &metav1.Condition{ Type: typeAuthenticatorValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return nil, conditions, nil @@ -260,23 +399,32 @@ func newWebhookAuthenticator( return nil, conditions, fmt.Errorf("%s: %w", errText, err) } - msg := "authenticator initialized" - conditions = append(conditions, &metav1.Condition{ - Type: typeAuthenticatorValid, - Status: metav1.ConditionTrue, - Reason: reasonSuccess, - Message: msg, - }) + conditions = append(conditions, successfulAuthenticatorValidCondition()) return webhookAuthenticator, conditions, nil } -func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPool, endpointHostPort *endpointaddr.HostPort, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) { +func successfulWebhookConnectionValidCondition() *metav1.Condition { + return &metav1.Condition{ + Type: typeWebhookConnectionValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "successfully dialed webhook server", + } +} + +func (c *webhookCacheFillerController) validateConnection( + certPool *x509.CertPool, + endpointHostPort *endpointaddr.HostPort, + conditions []*metav1.Condition, + prereqOk bool, + logger plog.Logger, +) ([]*metav1.Condition, error) { if !prereqOk { conditions = append(conditions, &metav1.Condition{ Type: typeWebhookConnectionValid, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: msgUnableToValidate, }) return conditions, nil @@ -290,7 +438,7 @@ func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPoo conditions = append(conditions, &metav1.Condition{ Type: typeWebhookConnectionValid, Status: metav1.ConditionFalse, - Reason: reasonUnableToDialServer, + Reason: conditionsutil.ReasonUnableToDialServer, Message: msg, }) return conditions, fmt.Errorf("%s: %w", errText, err) @@ -300,43 +448,13 @@ func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPoo err = conn.Close() if err != nil { // no unit test for this failure - c.log.Error("error closing dialer", err) + logger.Error("error closing dialer", err) } - conditions = append(conditions, &metav1.Condition{ - Type: typeWebhookConnectionValid, - Status: metav1.ConditionTrue, - Reason: reasonSuccess, - Message: "successfully dialed webhook server", - }) + conditions = append(conditions, successfulWebhookConnectionValidCondition()) return conditions, nil } -func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []byte, []*metav1.Condition, bool) { - rootCAs, pemBytes, err := pinnipedcontroller.BuildCertPoolAuth(tlsSpec) - if err != nil { - msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error()) - conditions = append(conditions, &metav1.Condition{ - Type: typeTLSConfigurationValid, - Status: metav1.ConditionFalse, - Reason: reasonInvalidTLSConfiguration, - Message: msg, - }) - return rootCAs, pemBytes, 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, pemBytes, conditions, true -} - func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditions []*metav1.Condition) (*endpointaddr.HostPort, []*metav1.Condition, bool) { endpointURL, err := url.Parse(endpoint) if err != nil { @@ -377,7 +495,7 @@ func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditi conditions = append(conditions, &metav1.Condition{ Type: typeEndpointURLValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.endpoint is a valid URL", }) return &endpointHostPort, conditions, true @@ -387,6 +505,7 @@ func (c *webhookCacheFillerController) updateStatus( ctx context.Context, original *authenticationv1alpha1.WebhookAuthenticator, conditions []*metav1.Condition, + logger plog.Logger, ) error { updated := original.DeepCopy() @@ -395,7 +514,7 @@ func (c *webhookCacheFillerController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionFalse, - Reason: reasonNotReady, + Reason: conditionsutil.ReasonNotReady, Message: "the WebhookAuthenticator is not ready: see other conditions for details", }) } else { @@ -403,7 +522,7 @@ func (c *webhookCacheFillerController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the WebhookAuthenticator is ready", }) } @@ -412,14 +531,19 @@ func (c *webhookCacheFillerController) updateStatus( conditions, original.Generation, &updated.Status.Conditions, - plog.New().WithName(controllerName), + logger, metav1.NewTime(c.clock.Now()), ) if equality.Semantic.DeepEqual(original, updated) { + logger.Debug("choosing to not update the webhookauthenticator status since there is no update to make", + "phase", updated.Status.Phase) return nil } - _, err := c.client.AuthenticationV1alpha1().WebhookAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + if err == nil { + logger.Debug("webhookauthenticator status successfully updated", + "phase", updated.Status.Phase) + } return err } diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go index b2c60418c..b75548a3a 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go @@ -16,15 +16,20 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "time" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" authenticationv1beta1 "k8s.io/api/authentication/v1beta1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/authentication/authenticator" + k8sinformers "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" "k8s.io/utils/ptr" @@ -34,11 +39,12 @@ import ( conciergeinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controller/authenticator/authncache" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/crypto/ptls" + "go.pinniped.dev/internal/mocks/mockcachevalue" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/testutil/conciergetestutil" "go.pinniped.dev/internal/testutil/conditionstestutil" "go.pinniped.dev/internal/testutil/tlsserver" ) @@ -118,12 +124,14 @@ func TestController(t *testing.T) { w.WriteHeader(http.StatusNotFound) _, _ = fmt.Fprint(w, "404 nothing here") })) - hostGoodDefaultServingCertServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hostGoodDefaultServingCertServer, hostGoodDefaultServingCertServerCAPEM := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux.ServeHTTP(w, r) }), func(s *httptest.Server) { tlsserver.AssertEveryTLSHello(t, s, ptls.Default) // assert on every hello because we are only expecting dials }) - + hostGoodDefaultServingCertServerTLSSpec := &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(hostGoodDefaultServingCertServerCAPEM), + } goodWebhookDefaultServingCertEndpoint := hostGoodDefaultServingCertServer.URL goodWebhookDefaultServingCertEndpointBut404 := goodWebhookDefaultServingCertEndpoint + "/nothing/here" @@ -142,7 +150,46 @@ func TestController(t *testing.T) { goodWebhookAuthenticatorSpecWithCA := authenticationv1alpha1.WebhookAuthenticatorSpec{ Endpoint: goodWebhookDefaultServingCertEndpoint, - TLS: conciergetestutil.TLSSpecFromTLSConfig(hostGoodDefaultServingCertServer.TLS), + TLS: hostGoodDefaultServingCertServerTLSSpec, + } + goodWebhookAuthenticatorSpecWithCAFromSecret := authenticationv1alpha1.WebhookAuthenticatorSpec{ + Endpoint: goodWebhookDefaultServingCertEndpoint, + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "secret-with-ca", + Key: "ca.crt", + }, + }, + } + someSecretWithCA := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret-with-ca", + Namespace: "concierge", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": hostGoodDefaultServingCertServerCAPEM, + }, + } + goodWebhookAuthenticatorSpecWithCAFromConfigMap := authenticationv1alpha1.WebhookAuthenticatorSpec{ + Endpoint: goodWebhookDefaultServingCertEndpoint, + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: "configmap-with-ca", + Key: "ca.crt", + }, + }, + } + someConfigMapWithCA := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap-with-ca", + Namespace: "concierge", + }, + Data: map[string]string{ + "ca.crt": string(hostGoodDefaultServingCertServerCAPEM), + }, } localWithExampleDotComWeebhookAuthenticatorSpec := authenticationv1alpha1.WebhookAuthenticatorSpec{ // CA for example.com, TLS serving cert for example.com, but endpoint is still localhost @@ -158,7 +205,7 @@ func TestController(t *testing.T) { } goodWebhookAuthenticatorSpecWith404Endpoint := authenticationv1alpha1.WebhookAuthenticatorSpec{ Endpoint: goodWebhookDefaultServingCertEndpointBut404, - TLS: conciergetestutil.TLSSpecFromTLSConfig(hostGoodDefaultServingCertServer.TLS), + TLS: hostGoodDefaultServingCertServerTLSSpec, } badWebhookAuthenticatorSpecInvalidTLS := authenticationv1alpha1.WebhookAuthenticatorSpec{ Endpoint: goodWebhookDefaultServingCertEndpoint, @@ -221,7 +268,7 @@ func TestController(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", - Message: "successfully parsed specified CA bundle", + Message: "spec.tls is valid: using configured CA bundle", } } happyTLSConfigurationValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition { @@ -231,7 +278,7 @@ func TestController(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "Success", - Message: "no CA bundle specified", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", } } sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition { @@ -240,8 +287,8 @@ func TestController(t *testing.T) { Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, - Reason: "InvalidTLSConfiguration", - Message: "invalid TLS configuration: illegal base64 data at input byte 7", + Reason: "InvalidTLSConfig", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7", } } @@ -360,27 +407,22 @@ func TestController(t *testing.T) { } tests := []struct { - name string - cache func(*testing.T, *authncache.Cache) - syncKey controllerlib.Key - webhooks []runtime.Object + name string + cache func(*testing.T, *authncache.Cache) + webhookAuthenticators []runtime.Object + secretsAndConfigMaps []runtime.Object // for modifying the clients to hack in arbitrary api responses - configClient func(*conciergefake.Clientset) - wantSyncLoopErr testutil.RequireErrorStringFunc - wantLogs []map[string]any - wantActions func() []coretesting.Action - wantCacheEntries int + configClient func(*conciergefake.Clientset) + wantSyncErr testutil.RequireErrorStringFunc + wantLogLines []string + wantActions func() []coretesting.Action + // random comment so lines above don't have huge indents + wantNamesOfWebhookAuthenticatorsInCache []string }{ { - name: "Sync: WebhookAuthenticator not found will abort sync loop, no status conditions", - syncKey: controllerlib.Key{Name: "test-name"}, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "Sync() found that the WebhookAuthenticator does not exist yet or was deleted", - }, + name: "Sync: No WebhookAuthenticators found results in no errors and no status conditions", + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).Sync","message":"No WebhookAuthenticators found"}`, }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -388,12 +430,11 @@ func TestController(t *testing.T) { coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "Sync: valid and unchanged WebhookAuthenticator: loop will preserve existing status conditions", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "Sync: valid and unchanged WebhookAuthenticator: loop will preserve existing status conditions", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -405,17 +446,9 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -423,25 +456,14 @@ func TestController(t *testing.T) { coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "Sync: valid and unchanged WebhookAuthenticator which was already cached: skips any updates to status or cache", - cache: func(t *testing.T, cache *authncache.Cache) { - cache.Store( - authncache.Key{ - Name: "test-name", - Kind: "WebhookAuthenticator", - APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, - }, - &cachedWebhookAuthenticator{spec: &goodWebhookAuthenticatorSpecWithCA}, - ) - }, - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "Sync: multiple valid and multiple invalid WebhookAuthenticators", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", + Name: "existing-webhook-authenticator", }, Spec: goodWebhookAuthenticatorSpecWithCA, Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ @@ -449,74 +471,39 @@ func TestController(t *testing.T) { Phase: "Ready", }, }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "actual webhook authenticator and desired webhook authenticator are the same", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, - }, - wantActions: func() []coretesting.Action { - return []coretesting.Action{ - coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), - coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), - } - }, - wantCacheEntries: 1, - }, - { - name: "Sync: authenticator update when cached authenticator is the wrong data type, which should never really happen: loop will complete successfully and update status conditions.", - cache: func(t *testing.T, cache *authncache.Cache) { - cache.Store( - authncache.Key{ - Name: "test-name", - Kind: "WebhookAuthenticator", - APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, - }, - // Only entries of type cachedWebhookAuthenticator are ever put into the cache, so this should never really happen. - // This test is to provide coverage on the production code which reads from the cache and casts those entries to - // the appropriate data type. - struct{ authenticator.Token }{}, - ) - }, - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", + Name: "new-webhook-authenticator", }, Spec: goodWebhookAuthenticatorSpecWithCA, }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "wrong webhook authenticator type in cache", - "actualType": "struct { authenticator.Token }", - }, - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-webhook-authenticator", }, + Spec: badWebhookAuthenticatorSpecInvalidTLS, }, + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-invalid-webhook-authenticator", + }, + Spec: badWebhookAuthenticatorSpecInvalidTLS, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"another-invalid-webhook-authenticator","endpoint":"%s","removedFromCache":false}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"another-invalid-webhook-authenticator","endpoint":"%s","phase":"Error"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"existing-webhook-authenticator","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"existing-webhook-authenticator","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"invalid-webhook-authenticator","endpoint":"%s","removedFromCache":false}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"invalid-webhook-authenticator","endpoint":"%s","phase":"Error"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"new-webhook-authenticator","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"new-webhook-authenticator","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), }, wantActions: func() []coretesting.Action { - updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + updateValidStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", + Name: "new-webhook-authenticator", }, Spec: goodWebhookAuthenticatorSpecWithCA, Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ @@ -524,215 +511,87 @@ func TestController(t *testing.T) { Phase: "Ready", }, }) - updateStatusAction.Subresource = "status" - return []coretesting.Action{ - coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), - coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), - updateStatusAction, - } - }, - wantCacheEntries: 1, - }, - { - name: "Sync: changed WebhookAuthenticator: loop will update timestamps only on relevant statuses", - cache: func(t *testing.T, cache *authncache.Cache) { - cache.Store( - authncache.Key{ - Name: "test-name", - Kind: "WebhookAuthenticator", - APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, - }, - &cachedWebhookAuthenticator{spec: &goodWebhookAuthenticatorSpecWith404Endpoint}, - ) - }, - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ - &authenticationv1alpha1.WebhookAuthenticator{ + updateValidStatusAction.Subresource = "status" + updateInvalidStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - Generation: 1234, + Name: "invalid-webhook-authenticator", }, - Spec: goodWebhookAuthenticatorSpecWithCA, - Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ - Conditions: conditionstestutil.Replace( - allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1233), - []metav1.Condition{ - sadReadyCondition(frozenTimeInThePast, 1232), - happyEndpointURLValid(frozenTimeInThePast, 1231), - }, - ), - Phase: "Ready", - }, - }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, - }, - wantActions: func() []coretesting.Action { - updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - Generation: 1234, - }, - Spec: goodWebhookAuthenticatorSpecWithCA, - Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ - Conditions: conditionstestutil.Replace( - allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1234), - []metav1.Condition{ - happyEndpointURLValid(frozenTimeInThePast, 1234), - }, - ), - Phase: "Ready", - }, - }) - updateStatusAction.Subresource = "status" - return []coretesting.Action{ - coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), - coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), - updateStatusAction, - } - }, - wantCacheEntries: 1, - }, - { - name: "Sync: valid WebhookAuthenticator with CA: will complete sync loop successfully with success conditions and ready phase", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ - &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: goodWebhookAuthenticatorSpecWithCA, - }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, - }, - wantActions: func() []coretesting.Action { - updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: goodWebhookAuthenticatorSpecWithCA, - Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ - Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), - Phase: "Ready", - }, - }) - updateStatusAction.Subresource = "status" - return []coretesting.Action{ - coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), - coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), - updateStatusAction, - } - }, - wantCacheEntries: 1, - }, - { - name: "Sync: valid WebhookAuthenticator with IPV6 and CA: will complete sync loop successfully with success conditions and ready phase", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ - &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: func() authenticationv1alpha1.WebhookAuthenticatorSpec { - ipv6 := goodWebhookAuthenticatorSpecWithCA.DeepCopy() - ipv6.Endpoint = hostLocalIPv6Server.URL - ipv6.TLS = ptr.To(authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(ipv6CA), - }) - return *ipv6 - }(), - }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": hostLocalIPv6Server.URL, - "webhook": map[string]any{ - "name": "test-name", - }, - }, - }, - wantActions: func() []coretesting.Action { - updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: func() authenticationv1alpha1.WebhookAuthenticatorSpec { - ipv6 := goodWebhookAuthenticatorSpecWithCA.DeepCopy() - ipv6.Endpoint = hostLocalIPv6Server.URL - ipv6.TLS = ptr.To(authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(ipv6CA), - }) - return *ipv6 - }(), - Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ - Conditions: allHappyConditionsSuccess(hostLocalIPv6Server.URL, frozenMetav1Now, 0), - Phase: "Ready", - }, - }) - updateStatusAction.Subresource = "status" - return []coretesting.Action{ - coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), - coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), - updateStatusAction, - } - }, - wantCacheEntries: 1, - }, - { - name: "Sync: valid WebhookAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ - &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: goodWebhookAuthenticatorSpecWithoutCA, - }, - }, - wantActions: func() []coretesting.Action { - updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - Spec: goodWebhookAuthenticatorSpecWithoutCA, + Spec: badWebhookAuthenticatorSpecInvalidTLS, Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ Conditions: conditionstestutil.Replace( allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), []metav1.Condition{ - happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), - sadWebhookConnectionValid(frozenMetav1Now, 0), - sadReadyCondition(frozenMetav1Now, 0), + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownWebhookConnectionValid(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), }, ), Phase: "Error", }, }) + updateInvalidStatusAction.Subresource = "status" + updateValidStatusAction.Subresource = "status" + updateAnotherInvalidStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-invalid-webhook-authenticator", + }, + Spec: badWebhookAuthenticatorSpecInvalidTLS, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + []metav1.Condition{ + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownWebhookConnectionValid(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateAnotherInvalidStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateAnotherInvalidStatusAction, + updateInvalidStatusAction, + updateValidStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{ + "existing-webhook-authenticator", + "new-webhook-authenticator", + }, + }, + { + name: "Sync: valid WebhookAuthenticator with CA from Secret: loop will complete successfully and update status conditions", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromSecret, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someSecretWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromSecret, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) updateStatusAction.Subresource = "status" return []coretesting.Action{ coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), @@ -740,13 +599,109 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority`), - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "validateTLS: WebhookAuthenticator with invalid CA will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "Sync: valid WebhookAuthenticator with CA from ConfigMap: loop will complete successfully and update status conditions", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromConfigMap, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someConfigMapWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromConfigMap, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: valid WebhookAuthenticator with external and changed CA bundle: loop will complete successfully and update status conditions", + cache: func(t *testing.T, cache *authncache.Cache) { + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWithCAFromConfigMap, "some-stale-ca-bundle-pem-content-from-secret"), + ) + }, + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromConfigMap, + }, + }, + secretsAndConfigMaps: []runtime.Object{ + someConfigMapWithCA, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":true}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCAFromConfigMap, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "previously valid cached authenticator (which did not specify a CA bundle) changes and becomes invalid due to any problem with the CA bundle: loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache", + cache: func(t *testing.T, cache *authncache.Cache) { + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + // Force an invalid spec into the cache, which is not very realistic, but it simulates a case + // where the CA bundle goes from being cached as empty to being an error during validation, + // without causing any changes in the spec. This test wants to prove that the rest of the + // validations get run and the resource is update, just in case that can happen somehow. + newCacheValue(t, badWebhookAuthenticatorSpecInvalidTLS, ""), + ) + }, + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -754,6 +709,10 @@ func TestController(t *testing.T) { Spec: badWebhookAuthenticatorSpecInvalidTLS, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":true}`, badWebhookAuthenticatorSpecInvalidTLS.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, goodWebhookDefaultServingCertEndpoint), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -780,22 +739,460 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "previously valid cached authenticator's spec changes and becomes invalid (e.g. spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache", + name: "Sync: valid and unchanged WebhookAuthenticator which was already cached: skips any updates to status or cache", cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWithCA.TLS.CertificateAuthorityData) + require.NoError(t, err) cache.Store( authncache.Key{ Name: "test-name", Kind: "WebhookAuthenticator", APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, }, - &cachedWebhookAuthenticator{spec: &goodWebhookAuthenticatorSpecWithCA}, + newCacheValue(t, goodWebhookAuthenticatorSpecWithCA, string(oldCA)), ) }, - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"cached webhook authenticator and desired webhook authenticator are the same: already cached, so skipping validations","webhookAuthenticator":"test-name","endpoint":"%s"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: authenticator update when cached authenticator is the wrong data type, which should never really happen: loop will complete successfully and update status conditions", + cache: func(t *testing.T, cache *authncache.Cache) { + ctrl := gomock.NewController(t) + t.Cleanup(func() { + ctrl.Finish() + }) + mockCacheValue := mockcachevalue.NewMockValue(ctrl) + mockCacheValue.EXPECT().Close().Times(1) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + // Only entries of type cachedWebhookAuthenticator are ever put into the cache, so this should never really happen. + // This test is to provide coverage on the production code which reads from the cache and casts those entries to + // the appropriate data type. + mockCacheValue, + ) + }, + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).cacheValueAsWebhookAuthenticator","message":"wrong webhook authenticator type in cache","webhookAuthenticator":"test-name","endpoint":"%s","actualType":"*mockcachevalue.MockValue"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: changed WebhookAuthenticator: loop will update timestamps only on relevant statuses", + cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWith404Endpoint.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWith404Endpoint, string(oldCA)), + ) + }, + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1233), + []metav1.Condition{ + sadReadyCondition(frozenTimeInThePast, 1232), + happyEndpointURLValid(frozenTimeInThePast, 1231), + }, + ), + Phase: "Ready", + }, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":true}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1234), + []metav1.Condition{ + happyEndpointURLValid(frozenTimeInThePast, 1234), + }, + ), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: previously cached authenticator gets new valid spec fields, but status update fails: loop will leave it in the cache", + cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWith404Endpoint.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWith404Endpoint, string(oldCA)), + ) + }, + configClient: func(client *conciergefake.Clientset) { + client.PrependReactor( + "update", + "webhookauthenticators", + func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("some update error") + }, + ) + }, + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + }, + }, + wantLogLines: nil, // wants no logs + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1234), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantSyncErr: testutil.WantExactErrorString("error for WebhookAuthenticator test-name: some update error"), + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, // keeps the old entry in the cache + }, + { + name: "Sync: previously cached valid authenticator with unchanged endpoint URL and CA bundle hash has invalid status conditions in informer cache, as can happen on subsequent sync soon after multiple quick status updates (when the informer cache finally catches up): should update status in current sync", + cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWithCA.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWithCA, string(oldCA)), + ) + }, + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + []metav1.Condition{ + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownWebhookConnectionValid(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"cached webhook authenticator and desired webhook authenticator are the same: already cached, so skipping validations","webhookAuthenticator":"test-name","endpoint":"%s"}`, goodWebhookAuthenticatorSpecWithCA.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookAuthenticatorSpecWithCA.Endpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Generation: 1234, + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ // updates the status to ready + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1234), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, // keeps the old entry in the cache + }, + { + name: "Sync: valid WebhookAuthenticator with CA: will complete sync loop successfully with success conditions and ready phase", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: valid WebhookAuthenticator with IPV6 and CA: will complete sync loop successfully with success conditions and ready phase", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: func() authenticationv1alpha1.WebhookAuthenticatorSpec { + ipv6 := goodWebhookAuthenticatorSpecWithCA.DeepCopy() + ipv6.Endpoint = hostLocalIPv6Server.URL + ipv6.TLS = ptr.To(authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(ipv6CA), + }) + return *ipv6 + }(), + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, hostLocalIPv6Server.URL), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, hostLocalIPv6Server.URL), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: func() authenticationv1alpha1.WebhookAuthenticatorSpec { + ipv6 := goodWebhookAuthenticatorSpecWithCA.DeepCopy() + ipv6.Endpoint = hostLocalIPv6Server.URL + ipv6.TLS = ptr.To(authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(ipv6CA), + }) + return *ipv6 + }(), + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: allHappyConditionsSuccess(hostLocalIPv6Server.URL, frozenMetav1Now, 0), + Phase: "Ready", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, + }, + { + name: "Sync: valid WebhookAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithoutCA, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, goodWebhookAuthenticatorSpecWithoutCA.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, goodWebhookAuthenticatorSpecWithoutCA.Endpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: goodWebhookAuthenticatorSpecWithoutCA, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + []metav1.Condition{ + happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), + sadWebhookConnectionValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantSyncErr: testutil.WantExactErrorString(`error for WebhookAuthenticator test-name: cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority`), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, + }, + { + name: "validateTLS: WebhookAuthenticator with invalid CA will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: badWebhookAuthenticatorSpecInvalidTLS, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, badWebhookAuthenticatorSpecInvalidTLS.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, badWebhookAuthenticatorSpecInvalidTLS.Endpoint), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: badWebhookAuthenticatorSpecInvalidTLS, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + []metav1.Condition{ + sadTLSConfigurationValid(frozenMetav1Now, 0), + unknownWebhookConnectionValid(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, + }, + { + name: "previously valid cached authenticator's spec changes and becomes invalid (e.g. spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache", + cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWithCA.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWithCA, string(oldCA)), + ) + }, + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -805,6 +1202,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":true}`, badEndpointInvalidURL), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, badEndpointInvalidURL), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -834,12 +1235,32 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, // removed from cache + wantNamesOfWebhookAuthenticatorsInCache: []string{}, // removed from cache }, { - name: "validateEndpoint: parsing error (spec.endpoint URL is invalid) will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "previously valid cached authenticator's spec changes and becomes invalid (e.g. spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, and will remove authenticator from cache even though the status update failed", + cache: func(t *testing.T, cache *authncache.Cache) { + oldCA, err := base64.StdEncoding.DecodeString(goodWebhookAuthenticatorSpecWithCA.TLS.CertificateAuthorityData) + require.NoError(t, err) + cache.Store( + authncache.Key{ + Name: "test-name", + Kind: "WebhookAuthenticator", + APIGroup: authenticationv1alpha1.SchemeGroupVersion.Group, + }, + newCacheValue(t, goodWebhookAuthenticatorSpecWithCA, string(oldCA)), + ) + }, + configClient: func(client *conciergefake.Clientset) { + client.PrependReactor( + "update", + "webhookauthenticators", + func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("some update error") + }, + ) + }, + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -849,6 +1270,9 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":true}`, badEndpointInvalidURL), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -878,12 +1302,59 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, + wantSyncErr: testutil.WantExactErrorString("error for WebhookAuthenticator test-name: some update error"), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, // removed from cache }, { - name: "validateEndpoint: parsing error (spec.endpoint URL has invalid scheme, requires https) will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateEndpoint: parsing error (spec.endpoint URL is invalid) will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", + webhookAuthenticators: []runtime.Object{ + &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: authenticationv1alpha1.WebhookAuthenticatorSpec{ + Endpoint: badEndpointInvalidURL, + }, + }, + }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, badEndpointInvalidURL), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, badEndpointInvalidURL), + }, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: authenticationv1alpha1.WebhookAuthenticatorSpec{ + Endpoint: badEndpointInvalidURL, + }, + Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), + []metav1.Condition{ + happyTLSConfigurationValidNoCA(frozenMetav1Now, 0), + sadEndpointURLValid("https://.café .com/café/café/café/coffee", frozenMetav1Now, 0), + unknownWebhookConnectionValid(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, + }, + { + name: "validateEndpoint: parsing error (spec.endpoint URL has invalid scheme, requires https) will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -893,6 +1364,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, badEndpointNoHTTPS), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, badEndpointNoHTTPS), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -922,12 +1397,11 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "validateEndpoint: should error if endpoint cannot be parsed", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateEndpoint: should error if endpoint cannot be parsed", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -940,6 +1414,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]:69999/some/fake/path","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]:69999/some/fake/path","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -971,12 +1449,11 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "validateConnection: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -984,7 +1461,11 @@ func TestController(t *testing.T) { Spec: badWebhookAuthenticatorSpecGoodEndpointButUnknownCA, }, }, - wantSyncLoopErr: testutil.WantExactErrorString("cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority"), + wantSyncErr: testutil.WantExactErrorString("error for WebhookAuthenticator test-name: cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority"), + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, badWebhookAuthenticatorSpecGoodEndpointButUnknownCA.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, badWebhookAuthenticatorSpecGoodEndpointButUnknownCA.Endpoint), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1010,15 +1491,14 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, - // No unit test for system roots. We don't test the JWTAuthenticator's use of system roots either. + // No unit test for system roots. We don't test the WebhookAuthenticator's use of system roots either. // We would have to find a way to mock out roots by adding a dummy cert in order to test this // { name: "validateConnection: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",}, { - name: "validateConnection: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1026,17 +1506,9 @@ func TestController(t *testing.T) { Spec: goodWebhookAuthenticatorSpecWith404Endpoint, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpointBut404, - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpointBut404), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpointBut404), }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ @@ -1056,12 +1528,11 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "validateConnection: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1079,17 +1550,9 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": fmt.Sprintf("https://localhost:%s", localhostURL.Port()), - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, fmt.Sprintf("https://localhost:%s", localhostURL.Port())), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, fmt.Sprintf("https://localhost:%s", localhostURL.Port())), }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -1097,12 +1560,11 @@ func TestController(t *testing.T) { coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "validateConnection: IPv6 address with port: should call dialer func with correct arguments", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: IPv6 address with port: should call dialer func with correct arguments", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1115,6 +1577,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]:4242/some/fake/path","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]:4242/some/fake/path","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1145,13 +1611,12 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`cannot dial server: dial tcp [::1]:4242: connect: connection refused`), - wantCacheEntries: 0, + wantSyncErr: testutil.WantExactErrorString(`error for WebhookAuthenticator test-name: cannot dial server: dial tcp [::1]:4242: connect: connection refused`), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "validateConnection: IPv6 address without port: should call dialer func with correct arguments", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: IPv6 address without port: should call dialer func with correct arguments", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1164,6 +1629,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]/some/fake/path","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"https://[0:0:0:0:0:0:0:1]/some/fake/path","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1194,13 +1663,12 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`cannot dial server: dial tcp [::1]:443: connect: connection refused`), - wantCacheEntries: 0, + wantSyncErr: testutil.WantExactErrorString(`error for WebhookAuthenticator test-name: cannot dial server: dial tcp [::1]:443: connect: connection refused`), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "validateConnection: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1217,17 +1685,9 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": hostAs127001WebhookServer.URL, - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, hostAs127001WebhookServer.URL), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, hostAs127001WebhookServer.URL), }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -1235,12 +1695,11 @@ func TestController(t *testing.T) { coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "validateConnection: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1252,6 +1711,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"%s","removedFromCache":false}`, localWithExampleDotComWeebhookAuthenticatorSpec.Endpoint), + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Error"}`, localWithExampleDotComWeebhookAuthenticatorSpec.Endpoint), + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1277,13 +1740,12 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 0, - wantSyncLoopErr: testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, + wantSyncErr: testutil.WantExactErrorString(`error for WebhookAuthenticator test-name: cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`), }, { - name: "validateConnection: IPv6 address without port or brackets: should succeed since IPv6 brackets are optional without port", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "validateConnection: IPv6 address without port or brackets: should succeed since IPv6 brackets are optional without port", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1296,6 +1758,10 @@ func TestController(t *testing.T) { }, }, }, + wantLogLines: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"invalid webhook authenticator","webhookAuthenticator":"test-name","endpoint":"https://0:0:0:0:0:0:0:1/some/fake/path","removedFromCache":false}`, + `{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"https://0:0:0:0:0:0:0:1/some/fake/path","phase":"Error"}`, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1326,13 +1792,12 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString(`cannot dial server: dial tcp [::1]:443: connect: connection refused`), - wantCacheEntries: 0, + wantSyncErr: testutil.WantExactErrorString(`error for WebhookAuthenticator test-name: cannot dial server: dial tcp [::1]:443: connect: connection refused`), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, }, { - name: "updateStatus: called with matching original and updated conditions: will not make request to update conditions", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "updateStatus: called with matching original and updated conditions: will not make request to update conditions", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1344,17 +1809,9 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"choosing to not update the webhookauthenticator status since there is no update to make","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), }, wantActions: func() []coretesting.Action { return []coretesting.Action{ @@ -1362,12 +1819,11 @@ func TestController(t *testing.T) { coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}), } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "updateStatus: called with different original and updated conditions: will make request to update conditions", - syncKey: controllerlib.Key{Name: "test-name"}, - webhooks: []runtime.Object{ + name: "updateStatus: called with different original and updated conditions: will make request to update conditions", + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", @@ -1384,17 +1840,9 @@ func TestController(t *testing.T) { }, }, }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, - }, + wantLogLines: []string{ + fmt.Sprintf(`{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).updateStatus","message":"webhookauthenticator status successfully updated","webhookAuthenticator":"test-name","endpoint":"%s","phase":"Ready"}`, goodWebhookDefaultServingCertEndpoint), + fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"webhookcachefiller-controller","caller":"webhookcachefiller/webhookcachefiller.go:$webhookcachefiller.(*webhookCacheFillerController).syncIndividualWebhookAuthenticator","message":"added or updated webhook authenticator in cache","webhookAuthenticator":"test-name","endpoint":"%s","isOverwrite":false}`, goodWebhookDefaultServingCertEndpoint), }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ @@ -1414,11 +1862,10 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantCacheEntries: 1, + wantNamesOfWebhookAuthenticatorsInCache: []string{"test-name"}, }, { - name: "updateStatus: when update request fails: error will enqueue a resync", - syncKey: controllerlib.Key{Name: "test-name"}, + name: "updateStatus: given a valid WebhookAuthenticator spec, when update request fails: error will enqueue a resync and the authenticator will not be added to the cache", configClient: func(client *conciergefake.Clientset) { client.PrependReactor( "update", @@ -1428,35 +1875,15 @@ func TestController(t *testing.T) { }, ) }, - webhooks: []runtime.Object{ + webhookAuthenticators: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: "test-name", }, Spec: goodWebhookAuthenticatorSpecWithCA, - Status: authenticationv1alpha1.WebhookAuthenticatorStatus{ - Conditions: conditionstestutil.Replace( - allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0), - []metav1.Condition{ - sadReadyCondition(frozenMetav1Now, 0), - }, - ), - Phase: "SomethingBeforeUpdating", - }, - }, - }, - wantLogs: []map[string]any{ - { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "webhookcachefiller-controller", - "message": "added new webhook authenticator", - "endpoint": goodWebhookDefaultServingCertEndpoint, - "webhook": map[string]any{ - "name": "test-name", - }, }, }, + wantLogLines: nil, // wants no logs wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1475,19 +1902,20 @@ func TestController(t *testing.T) { updateStatusAction, } }, - wantSyncLoopErr: testutil.WantExactErrorString("some update error"), - wantCacheEntries: 1, + wantSyncErr: testutil.WantExactErrorString("error for WebhookAuthenticator test-name: some update error"), + wantNamesOfWebhookAuthenticatorsInCache: []string{}, // even though the authenticator was valid, do not cache it because the status update failed }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - pinnipedAPIClient := conciergefake.NewSimpleClientset(tt.webhooks...) + pinnipedAPIClient := conciergefake.NewSimpleClientset(tt.webhookAuthenticators...) if tt.configClient != nil { tt.configClient(pinnipedAPIClient) } - informers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + kubeInformers := kubeinformers.NewSharedInformerFactory(kubernetesfake.NewSimpleClientset(tt.secretsAndConfigMaps...), 0) cache := authncache.New() var log bytes.Buffer @@ -1498,50 +1926,45 @@ func TestController(t *testing.T) { } controller := New( + "concierge", // namespace for controller cache, pinnipedAPIClient, - informers.Authentication().V1alpha1().WebhookAuthenticators(), + pinnipedInformers.Authentication().V1alpha1().WebhookAuthenticators(), + kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), + controllerlib.WithInformer, frozenClock, logger) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - informers.Start(ctx.Done()) + pinnipedInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) - syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey} + syncCtx := controllerlib.Context{Context: ctx} - if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncLoopErr != nil { - testutil.RequireErrorStringFromErr(t, err, tt.wantSyncLoopErr) + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncErr != nil { + testutil.RequireErrorStringFromErr(t, err, tt.wantSyncErr) } else { require.NoError(t, err) } - actualLogLines := testutil.SplitByNewline(log.String()) - require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct") - - for logLineNum, logLine := range actualLogLines { - require.NotNil(t, tt.wantLogs[logLineNum], "expected log line should never be empty") - var lineStruct map[string]any - err := json.Unmarshal([]byte(logLine), &lineStruct) - require.NoError(t, err) - require.Equal(t, tt.wantLogs[logLineNum]["level"], lineStruct["level"], fmt.Sprintf("log line (%d) log level should be correct (in: %s)", logLineNum, lineStruct)) - - require.Equal(t, tt.wantLogs[logLineNum]["timestamp"], lineStruct["timestamp"], fmt.Sprintf("log line (%d) timestamp should be correct (in: %s)", logLineNum, lineStruct)) - require.Equal(t, lineStruct["logger"], tt.wantLogs[logLineNum]["logger"], fmt.Sprintf("log line (%d) logger should be correct", logLineNum)) - require.NotEmpty(t, lineStruct["caller"], fmt.Sprintf("log line (%d) caller should not be empty", logLineNum)) - require.Equal(t, tt.wantLogs[logLineNum]["message"], lineStruct["message"], fmt.Sprintf("log line (%d) message should be correct", logLineNum)) - if lineStruct["webhook"] != nil { - require.Equal(t, tt.wantLogs[logLineNum]["webhook"], lineStruct["webhook"], fmt.Sprintf("log line (%d) webhook should be correct", logLineNum)) - } - if lineStruct["endpoint"] != nil { - require.Equal(t, tt.wantLogs[logLineNum]["endpoint"], lineStruct["endpoint"], fmt.Sprintf("log line (%d) endpoint should be correct", logLineNum)) - } - } require.NotEmpty(t, tt.wantActions, "wantActions is required for test %s", tt.name) require.Equal(t, tt.wantActions(), pinnipedAPIClient.Actions()) - require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys())) + require.Equal(t, len(tt.wantNamesOfWebhookAuthenticatorsInCache), len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", len(tt.wantNamesOfWebhookAuthenticatorsInCache), len(cache.Keys()), cache.Keys())) + + actualLog, _ := strings.CutSuffix(log.String(), "\n") + actualLogLines := strings.Split(actualLog, "\n") + var webhookLogLines []string + for _, line := range actualLogLines { + if strings.Contains(line, "webhookcachefiller/webhookcachefiller.go") { + webhookLogLines = append(webhookLogLines, line) + } + } + + require.Equal(t, tt.wantLogLines, webhookLogLines) }) } } @@ -1671,3 +2094,158 @@ func TestNewWebhookAuthenticator(t *testing.T) { }) } } + +func newCacheValue(t *testing.T, spec authenticationv1alpha1.WebhookAuthenticatorSpec, caBundle string) authncache.Value { + t.Helper() + + return &cachedWebhookAuthenticator{ + endpoint: spec.Endpoint, + caBundleHash: tlsconfigutil.NewCABundleHash([]byte(caBundle)), + } +} + +func TestControllerFilterSecret(t *testing.T) { + tests := []struct { + name string + secret metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "should return true for a secret of the type Opaque", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return true for a secret of the type TLS", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", + secret: &corev1.Secret{ + Type: "other-type", + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, + }, + { + name: "should return false for a resource of wrong data type", + secret: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClock := clocktesting.NewFakeClock(nowDoesntMatter) + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + pinnipedAPIClient := conciergefake.NewSimpleClientset() + pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + "concierge", // namespace for controller + authncache.New(), + pinnipedAPIClient, + pinnipedInformers.Authentication().V1alpha1().WebhookAuthenticators(), + secretInformer, + configMapInformer, + observableInformers.WithInformer, + frozenClock, + logger) + + unrelated := &corev1.Secret{} + filter := observableInformers.GetFilterForInformer(secretInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.secret, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.secret)) + }) + } +} + +func TestControllerFilterConfigMap(t *testing.T) { + namespace := "some-namespace" + goodCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "a configMap in the right namespace", + cm: goodCM, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClock := clocktesting.NewFakeClock(nowDoesntMatter) + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + pinnipedAPIClient := conciergefake.NewSimpleClientset() + pinnipedInformers := conciergeinformers.NewSharedInformerFactory(pinnipedAPIClient, 0) + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + "concierge", // namespace for the controller + authncache.New(), + pinnipedAPIClient, + pinnipedInformers.Authentication().V1alpha1().WebhookAuthenticators(), + secretInformer, + configMapInformer, + observableInformers.WithInformer, + frozenClock, + logger) + + unrelated := &corev1.ConfigMap{} + filter := observableInformers.GetFilterForInformer(configMapInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.cm, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.cm)) + }) + } +} diff --git a/internal/controller/conditionsutil/conditions_util.go b/internal/controller/conditionsutil/conditions_util.go index 38004b907..91bc24c63 100644 --- a/internal/controller/conditionsutil/conditions_util.go +++ b/internal/controller/conditionsutil/conditions_util.go @@ -12,6 +12,15 @@ import ( "go.pinniped.dev/internal/plog" ) +// Some common reasons shared by conditions of various resources. +const ( + ReasonSuccess = "Success" + ReasonNotReady = "NotReady" + ReasonUnableToValidate = "UnableToValidate" + ReasonUnableToDialServer = "UnableToDialServer" + ReasonInvalidIssuerURL = "InvalidIssuerURL" +) + // MergeConditions merges conditions into conditionsToUpdate. // Note that LastTransitionTime refers to the time when the status changed, // but ObservedGeneration should be the current generation for all conditions, since Pinniped should always check every condition. diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index efe64d055..1a4865eed 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -14,6 +14,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/google/uuid" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -155,7 +156,7 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont return &metav1.Condition{ Type: upstreamwatchers.TypeSearchBaseFound, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "Successfully fetched defaultNamingContext to use as default search base from RootDSE.", } } @@ -235,6 +236,7 @@ type activeDirectoryWatcherController struct { client supervisorclientset.Interface activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer secretInformer corev1informers.SecretInformer + configMapInformer corev1informers.ConfigMapInformer } // New instantiates a new controllerlib.Controller which will populate the provided UpstreamActiveDirectoryIdentityProviderICache. @@ -243,6 +245,7 @@ func New( client supervisorclientset.Interface, activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return newInternal( @@ -254,6 +257,7 @@ func New( client, activeDirectoryIdentityProviderInformer, secretInformer, + configMapInformer, withInformer, ) } @@ -266,6 +270,7 @@ func newInternal( client supervisorclientset.Interface, activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { c := activeDirectoryWatcherController{ @@ -275,6 +280,7 @@ func newInternal( client: client, activeDirectoryIdentityProviderInformer: activeDirectoryIdentityProviderInformer, secretInformer: secretInformer, + configMapInformer: configMapInformer, } return controllerlib.New( controllerlib.Config{Name: activeDirectoryControllerName, Syncer: &c}, @@ -285,7 +291,18 @@ func newInternal( ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + upstreamwatchers.LDAPBindAccountSecretType, + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) @@ -357,7 +374,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, } } - conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSettingsCache, config) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.configMapInformer, c.validatedSettingsCache, config) c.updateStatus(ctx, upstream, conditions.Conditions()) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 80e47d7ca..42cee8d20 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -27,6 +27,7 @@ import ( supervisorinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" @@ -47,7 +48,7 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterSecrets(t *testing.T) { wantDelete bool }{ { - name: "a secret of the right type", + name: "should return true for a secret of type BasicAuth", secret: &corev1.Secret{ Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, @@ -57,14 +58,34 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterSecrets(t *testing.T) { wantDelete: true, }, { - name: "a secret of the wrong type", + name: "should return true for a secret of type TLS", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return true for a secret of type Opaque", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", secret: &corev1.Secret{ Type: "this-is-the-wrong-type", ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, }, { - name: "resource of a data type which is not watched by this controller", + name: "should return false for a resource of a data type which is not watched by this controller", secret: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, @@ -80,9 +101,10 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterSecrets(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() withInformer := testutil.NewObservableWithInformerOption() - New(nil, nil, activeDirectoryIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, activeDirectoryIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(secretInformer) @@ -94,6 +116,51 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterSecrets(t *testing.T) { } } +func TestActiveDirectoryUpstreamWatcherControllerFilterConfigMaps(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any configmap", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fakePinnipedClient := supervisorfake.NewSimpleClientset() + pinnipedInformers := supervisorinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + activeDirectoryIDPInformer := pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + withInformer := testutil.NewObservableWithInformerOption() + + New(nil, nil, activeDirectoryIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(configMapInformer) + require.Equal(t, test.wantAdd, filter.Add(test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(test.cm, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(test.cm)) + }) + } +} + func TestActiveDirectoryUpstreamWatcherControllerFilterActiveDirectoryIdentityProviders(t *testing.T) { t.Parallel() @@ -124,9 +191,10 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterActiveDirectoryIdentityPr fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() withInformer := testutil.NewObservableWithInformerOption() - New(nil, nil, activeDirectoryIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, activeDirectoryIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(activeDirectoryIDPInformer) @@ -167,6 +235,9 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { testGroupSearchFilter = "test-group-search-filter" testGroupSearchUserAttributeForFilter = "test-group-search-filter-user-attr-for-filter" testGroupSearchNameAttrName = "test-group-name-attr" + + caBundleConfigMapName = "test-ca-bundle-cm" + caBundleSecretName = "test-ca-bundle-secret" //nolint:gosec // this is not a credential ) testValidSecretData := map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} @@ -201,6 +272,51 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, } + + validUpstreamWithConfigMapCABundleSource := validUpstream.DeepCopy() + validUpstreamWithConfigMapCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithConfigMapCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caBundleConfigMapName, + Key: "ca.crt", + } + caBundleConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleConfigMapName, Namespace: testNamespace}, + Data: map[string]string{ + "ca.crt": string(testCABundle), + }, + } + + validUpstreamWithOpaqueSecretCABundleSource := validUpstream.DeepCopy() + validUpstreamWithOpaqueSecretCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithOpaqueSecretCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caBundleSecretName, + Key: "ca.crt", + } + caBundleOpaqueSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": testCABundle, + }, + } + + validUpstreamWithTLSSecretCABundleSource := validUpstream.DeepCopy() + validUpstreamWithTLSSecretCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithTLSSecretCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caBundleSecretName, + Key: "ca.crt", + } + caBundleTLSSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": testCABundle, + }, + } + editedValidUpstream := func(editFunc func(*idpv1alpha1.ActiveDirectoryIdentityProvider)) *idpv1alpha1.ActiveDirectoryIdentityProvider { deepCopy := validUpstream.DeepCopy() editFunc(deepCopy) @@ -275,13 +391,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { c.LastTransitionTime = metav1.Time{} return c } - tlsConfigurationValidLoadedTrueCondition := func(gen int64) metav1.Condition { + tlsConfigurationValidLoadedTrueCondition := func(gen int64, msg string) metav1.Condition { return metav1.Condition{ Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "loaded TLS configuration", + Message: fmt.Sprintf("spec.tls is valid: %s", msg), ObservedGeneration: gen, } } @@ -324,7 +440,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(gen), activeDirectoryConnectionValidTrueCondition(gen, secretVersion), searchBaseFoundInConfigCondition(gen), - tlsConfigurationValidLoadedTrueCondition(gen), + tlsConfigurationValidLoadedTrueCondition(gen, "using configured CA bundle"), } } @@ -368,7 +484,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { name string initialValidatedSettings map[string]upstreamwatchers.ValidatedSettings inputUpstreams []runtime.Object - inputSecrets []runtime.Object + inputK8sObjects []runtime.Object setupMocks func(conn *mockldapconn.MockConn) dialErrors map[string]error wantErr string @@ -381,9 +497,9 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{}, }, { - name: "one valid upstream updates the cache to include only that upstream", - inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + name: "one valid upstream using a configmap to source ca bundle should include that one upstream", + inputUpstreams: []runtime.Object{validUpstreamWithConfigMapCABundleSource}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242"), caBundleConfigMap}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -402,6 +518,127 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), + }}, + }, + { + name: "valid upstream spec using a configmap to source CA bundles that is already in the cache is updated to have a new ca bundle: Sync should now update the cache with the new CA bundle hash", + inputUpstreams: []runtime.Object{validUpstreamWithConfigMapCABundleSource}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242"), caBundleConfigMap}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash([]byte("this CA bundle should be replaced")), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, + Status: idpv1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), + }}, + }, + { + name: "one valid upstream using an opaque secret to source ca bundle should include that one upstream", + inputUpstreams: []runtime.Object{validUpstreamWithOpaqueSecretCABundleSource}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242"), caBundleOpaqueSecret}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, + Status: idpv1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), + }}, + }, + { + name: "one valid upstream using a TLS secret to source ca bundle should include that one upstream", + inputUpstreams: []runtime.Object{validUpstreamWithTLSSecretCABundleSource}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242"), caBundleTLSSecret}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, + Status: idpv1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), + }}, + }, + { + name: "one valid upstream updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{validUpstream}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, + Status: idpv1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -410,7 +647,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { { name: "missing secret", inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{}, + inputK8sObjects: []runtime.Object{}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ @@ -426,7 +663,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -434,7 +671,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { { name: "secret has wrong type", inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputK8sObjects: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testBindSecretName, Namespace: testNamespace}, Type: "some-other-type", Data: testValidSecretData, @@ -454,7 +691,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -462,7 +699,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { { name: "secret is missing key", inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputK8sObjects: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testBindSecretName, Namespace: testNamespace}, Type: corev1.SecretTypeBasicAuth, }}, @@ -481,7 +718,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -491,7 +728,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" })}, - inputSecrets: []runtime.Object{validBindUserSecret("")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("")}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ @@ -505,7 +742,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", - Message: "certificateAuthorityData is invalid: illegal base64 data at input byte 4", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4", ObservedGeneration: 1234, }, }, @@ -517,7 +754,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) })}, - inputSecrets: []runtime.Object{validBindUserSecret("")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("")}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ @@ -531,7 +768,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", - Message: "certificateAuthorityData is invalid: no certificates found", + Message: `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, ObservedGeneration: 1234, }, }, @@ -543,7 +780,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.TLS = nil })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -591,7 +828,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "no TLS configuration provided", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", ObservedGeneration: 1234, }, }, @@ -602,6 +839,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(nil), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -613,7 +851,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.TLS = nil upstream.Spec.GroupSearch.Attributes.GroupName = "sAMAccountName" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -661,7 +899,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "no TLS configuration provided", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", ObservedGeneration: 1234, }, }, @@ -672,6 +910,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(nil), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -682,7 +921,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.Host = "ldap.example.com" // when the port is not specified, automatically switch ports for StartTLS })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -738,7 +977,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: 1234, }, searchBaseFoundInConfigCondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -748,6 +987,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, IDPSpecGeneration: 1234, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), ConnectionValidCondition: &metav1.Condition{ Type: "LDAPConnectionValid", Status: "True", @@ -764,7 +1004,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.Host = "ldap.example.com:5678" // when the port is specified, do not automatically switch ports for StartTLS })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Both dials fail, so there should be no bind. }, @@ -819,7 +1059,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: 1234, }, searchBaseFoundInConfigCondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -830,7 +1070,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -868,8 +1108,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingUpstreams: []idpv1alpha1.ActiveDirectoryIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, Status: idpv1alpha1.ActiveDirectoryIdentityProviderStatus{ - Phase: "Ready", - Conditions: allConditionsTrue(1234, "4242"), + Phase: "Ready", + Conditions: []metav1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInConfigCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "no TLS configuration provided: using default root CA bundle from container image"), + }, }, }}, wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { @@ -877,6 +1122,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(nil), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -890,7 +1136,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.Bind.SecretName = "non-existent-secret" upstream.UID = "other-uid" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind for the one valid upstream configuration. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -912,7 +1158,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, "non-existent-secret"), ObservedGeneration: 42, }, - tlsConfigurationValidLoadedTrueCondition(42), + tlsConfigurationValidLoadedTrueCondition(42, "using configured CA bundle"), }, }, }, @@ -929,6 +1175,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -938,8 +1185,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway but not to validatedsettings (treated like a warning)", // If we can't connect, we can still try to allow users to log in, but update the conditions to say that there's a problem // Also don't add anything to the validated settings so that the next time this runs we can try again. - inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{validBindUserSecret("")}, + inputUpstreams: []runtime.Object{validUpstream}, + inputK8sObjects: []runtime.Object{validBindUserSecret("")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. // Expect two calls to each of these: once for trying TLS and once for trying StartTLS. @@ -965,7 +1212,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: 1234, }, searchBaseFoundInConfigCondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -977,7 +1224,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. // Expect three calls bind: once for trying TLS and once for trying StartTLS (both fail), and one before querying for defaultNamingContext (succeeds) @@ -1033,7 +1280,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: 1234, }, searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1044,7 +1291,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. // Expect 3 calls to each of these: once for trying TLS and once for trying StartTLS, one before querying @@ -1069,7 +1316,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: 1234, }, searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before querying for defaultNamingContext: some bind error"), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1084,12 +1331,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { searchBaseFoundInConfigCondition(1234), } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1110,6 +1358,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1126,7 +1375,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } upstream.Spec.UserSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, @@ -1173,7 +1422,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1181,6 +1430,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), GroupSearchBase: testGroupSearchBase, IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), @@ -1197,12 +1447,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } upstream.Spec.UserSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1246,7 +1497,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1255,6 +1506,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1269,11 +1521,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { searchBaseFoundInConfigCondition(1234), } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS, IDPSpecGeneration: 1234, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithStartTLS.CABundle), UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), @@ -1295,6 +1548,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.StartTLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithStartTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1308,12 +1562,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1233, "4242"), // older spec generation! } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1233, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1336,6 +1591,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1349,13 +1605,14 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1234, "4200"), // old version of the condition, as if the previous update of conditions had failed } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, IDPSpecGeneration: 1234, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), // already previously validated with version 4242 SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), }}, @@ -1376,6 +1633,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1396,7 +1654,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -1415,6 +1673,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1428,12 +1687,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4241")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1456,6 +1716,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1469,7 +1730,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.GroupSearch.Filter = "" upstream.Spec.GroupSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{} })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -1517,6 +1778,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -1529,7 +1791,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Base = "" upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1573,7 +1835,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1582,6 +1844,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: exampleDefaultNamingContext, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1593,7 +1856,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.UserSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1637,7 +1900,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1646,6 +1909,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1657,7 +1921,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1701,7 +1965,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1710,6 +1974,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: exampleDefaultNamingContext, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1721,7 +1986,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1737,7 +2002,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: some error"), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1749,7 +2014,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1773,7 +2038,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: empty search base DN found"), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1785,7 +2050,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1815,7 +2080,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: expected to find 1 entry but found 2"), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1827,7 +2092,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) @@ -1844,7 +2109,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: expected to find 1 entry but found 0"), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1860,12 +2125,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Spec.UserSearch.Attributes = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4241")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1913,7 +2179,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), searchBaseFoundInRootDSECondition(1234), - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -1922,6 +2188,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, GroupSearchBase: exampleDefaultNamingContext, UserSearchBase: testUserSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInRootDSECondition(0))), @@ -1932,7 +2199,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *idpv1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.GroupSearch.SkipGroupRefresh = true })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputK8sObjects: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -1981,7 +2248,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "loaded TLS configuration", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234, }, }, @@ -1992,6 +2259,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(activeDirectoryConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), SearchBaseFoundCondition: condPtr(withoutTime(searchBaseFoundInConfigCondition(0))), @@ -2005,7 +2273,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { fakePinnipedClient := supervisorfake.NewSimpleClientset(tt.inputUpstreams...) pinnipedInformers := supervisorinformers.NewSharedInformerFactory(fakePinnipedClient, 0) - fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) + fakeKubeClient := fake.NewSimpleClientset(tt.inputK8sObjects...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ @@ -2048,6 +2316,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), controllerlib.WithInformer, ) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index b15173819..715b2b36f 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -48,10 +48,6 @@ const ( typeTransformsExpressionsValid = "TransformsExpressionsValid" typeTransformsExamplesPassed = "TransformsExamplesPassed" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" - reasonInvalidIssuerURL = "InvalidIssuerURL" reasonDuplicateIssuer = "DuplicateIssuer" reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess" @@ -673,7 +669,7 @@ func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuf conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersObjectRefKindValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", }) } @@ -701,7 +697,7 @@ func appendIdentityProvidersFoundCondition( conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the resources specified by .spec.identityProviders[].objectRef were found", }) } @@ -721,7 +717,7 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersAPIGroupSuffixValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", }) } @@ -740,7 +736,7 @@ func appendTransformsExpressionsValidCondition(messages []string, conditions []* conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExpressionsValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", }) } @@ -759,7 +755,7 @@ func appendTransformsExamplesPassedCondition(messages []string, conditions []*me conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExamplesPassed, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", }) } @@ -779,7 +775,7 @@ func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersDisplayNamesUnique, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "the names specified by .spec.identityProviders[].displayName are unique", }) } @@ -793,14 +789,14 @@ func appendIssuerURLValidCondition(err error, conditions []*metav1.Condition) [] conditions = append(conditions, &metav1.Condition{ Type: typeIssuerURLValid, Status: metav1.ConditionFalse, - Reason: reasonInvalidIssuerURL, + Reason: conditionsutil.ReasonInvalidIssuerURL, Message: err.Error(), }) } else { conditions = append(conditions, &metav1.Condition{ Type: typeIssuerURLValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.issuer is a valid URL", }) } @@ -819,7 +815,7 @@ func (c *federationDomainWatcherController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionFalse, - Reason: reasonNotReady, + Reason: conditionsutil.ReasonNotReady, Message: "the FederationDomain is not ready: see other conditions for details", }) } else { @@ -827,7 +823,7 @@ func (c *federationDomainWatcherController) updateStatus( conditions = append(conditions, &metav1.Condition{ Type: typeReady, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ "the discovery endpoint is %s/.well-known/openid-configuration", federationDomain.Spec.Issuer), }) @@ -886,13 +882,13 @@ func (v *crossFederationDomainConfigValidator) Validate(federationDomain *superv conditions = append(conditions, &metav1.Condition{ Type: typeIssuerIsUnique, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", }) conditions = append(conditions, &metav1.Condition{ Type: typeOneTLSSecretPerIssuerHostname, Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Reason: conditionsutil.ReasonUnableToValidate, Message: "unable to check if all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL because URL cannot be parsed", }) return conditions @@ -909,7 +905,7 @@ func (v *crossFederationDomainConfigValidator) Validate(federationDomain *superv conditions = append(conditions, &metav1.Condition{ Type: typeIssuerIsUnique, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.issuer is unique among all FederationDomains", }) } @@ -925,7 +921,7 @@ func (v *crossFederationDomainConfigValidator) Validate(federationDomain *superv conditions = append(conditions, &metav1.Condition{ Type: typeOneTLSSecretPerIssuerHostname, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", }) } diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go index c9580d98b..762737235 100644 --- a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go @@ -7,13 +7,13 @@ package githubupstreamwatcher import ( "context" "crypto/tls" - "crypto/x509" "errors" "fmt" "net" "net/http" "slices" "strings" + "time" "golang.org/x/oauth2" corev1 "k8s.io/api/core/v1" @@ -21,6 +21,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/cache" utilerrors "k8s.io/apimachinery/pkg/util/errors" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/utils/clock" @@ -31,6 +32,7 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" @@ -57,6 +59,9 @@ const ( GitHubConnectionValid string = "GitHubConnectionValid" ClaimsValid string = "ClaimsValid" + reasonInvalid = "Invalid" + reasonInvalidHost = "InvalidHost" + defaultHost = "github.com" defaultApiBaseURL = "https://api.github.com" ) @@ -66,6 +71,43 @@ type UpstreamGitHubIdentityProviderICache interface { SetGitHubIdentityProviders([]upstreamprovider.UpstreamGithubIdentityProviderI) } +type GitHubValidatedAPICacheI interface { + MarkAsValidated(address string, caBundleHash tlsconfigutil.CABundleHash) + IsValid(address string, caBundleHash tlsconfigutil.CABundleHash) bool +} + +type GitHubValidatedAPICache struct { + cache *cache.Expiring +} + +type GitHubValidatedAPICacheKey struct { + address string + caBundleHash tlsconfigutil.CABundleHash +} + +func (g *GitHubValidatedAPICache) MarkAsValidated(address string, caBundleHash tlsconfigutil.CABundleHash) { + key := GitHubValidatedAPICacheKey{ + address: address, + caBundleHash: caBundleHash, + } + // Existence in the cache means it has been validated. + // The TTL in the cache is not important, it's just a "really long time". + g.cache.Set(key, nil, 365*24*time.Hour) +} + +func (g *GitHubValidatedAPICache) IsValid(address string, caBundleHash tlsconfigutil.CABundleHash) bool { + key := GitHubValidatedAPICacheKey{ + address: address, + caBundleHash: caBundleHash, + } + _, ok := g.cache.Get(key) + return ok +} + +func NewGitHubValidatedAPICache(cache *cache.Expiring) GitHubValidatedAPICacheI { + return &GitHubValidatedAPICache{cache: cache} +} + type gitHubWatcherController struct { namespace string cache UpstreamGitHubIdentityProviderICache @@ -73,8 +115,10 @@ type gitHubWatcherController struct { client supervisorclientset.Interface gitHubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer secretInformer corev1informers.SecretInformer + configMapInformer corev1informers.ConfigMapInformer clock clock.Clock dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error) + validatedCache GitHubValidatedAPICacheI } // New instantiates a new controllerlib.Controller which will populate the provided UpstreamGitHubIdentityProviderICache. @@ -84,10 +128,12 @@ func New( client supervisorclientset.Interface, gitHubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, log plog.Logger, withInformer pinnipedcontroller.WithInformerOptionFunc, clock clock.Clock, dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error), + validatedCache *cache.Expiring, ) controllerlib.Controller { c := gitHubWatcherController{ namespace: namespace, @@ -96,23 +142,34 @@ func New( log: log.WithName(controllerName), gitHubIdentityProviderInformer: gitHubIdentityProviderInformer, secretInformer: secretInformer, + configMapInformer: configMapInformer, clock: clock, dialFunc: dialFunc, + validatedCache: NewGitHubValidatedAPICache(validatedCache), } return controllerlib.New( controllerlib.Config{Name: controllerName, Syncer: &c}, withInformer( gitHubIdentityProviderInformer, - pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { - gitHubIDP, ok := obj.(*idpv1alpha1.GitHubIdentityProvider) - return ok && gitHubIDP.Namespace == namespace - }, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(gitHubClientSecretType, pinnipedcontroller.SingletonQueue(), namespace), + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + gitHubClientSecretType, + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue(), + ), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) @@ -201,7 +258,7 @@ func (c *gitHubWatcherController) validateClientSecret(secretName string) (*meta return &metav1.Condition{ Type: ClientCredentialsSecretValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", secretName), }, clientID, clientSecret, nil } @@ -219,7 +276,7 @@ func validateOrganizationsPolicy(organizationsSpec *idpv1alpha1.GitHubOrganizati return &metav1.Condition{ Type: OrganizationsPolicyValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.allowAuthentication.organizations.policy (%q) is valid", policy), } } @@ -228,7 +285,7 @@ func validateOrganizationsPolicy(organizationsSpec *idpv1alpha1.GitHubOrganizati return &metav1.Condition{ Type: OrganizationsPolicyValid, Status: metav1.ConditionFalse, - Reason: "Invalid", + Reason: reasonInvalid, Message: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", } } @@ -236,7 +293,7 @@ func validateOrganizationsPolicy(organizationsSpec *idpv1alpha1.GitHubOrganizati return &metav1.Condition{ Type: OrganizationsPolicyValid, Status: metav1.ConditionFalse, - Reason: "Invalid", + Reason: reasonInvalid, Message: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", } } @@ -266,13 +323,19 @@ func (c *gitHubWatcherController) validateUpstreamAndUpdateConditions(ctx contro hostCondition, hostPort := validateHost(upstream.Spec.GitHubAPI) conditions = append(conditions, hostCondition) - tlsConfigCondition, certPool := c.validateTLSConfiguration(upstream.Spec.GitHubAPI.TLS) + tlsConfigCondition, caBundle := tlsconfigutil.ValidateTLSConfig( + tlsconfigutil.TLSSpecForSupervisor(upstream.Spec.GitHubAPI.TLS), + "spec.githubAPI.tls", + c.namespace, + c.secretInformer, + c.configMapInformer) conditions = append(conditions, tlsConfigCondition) githubConnectionCondition, hostURL, httpClient, githubConnectionErr := c.validateGitHubConnection( hostPort, - certPool, - hostCondition.Status == metav1.ConditionTrue && tlsConfigCondition.Status == metav1.ConditionTrue, + caBundle, + hostCondition.Status == metav1.ConditionTrue, + tlsConfigCondition.Status == metav1.ConditionTrue, ) if githubConnectionErr != nil { applicationErrors = append(applicationErrors, githubConnectionErr) @@ -334,7 +397,7 @@ func validateHost(gitHubAPIConfig idpv1alpha1.GitHubAPIConfig) (*metav1.Conditio return &metav1.Condition{ Type: HostValid, Status: metav1.ConditionFalse, - Reason: "InvalidHost", + Reason: reasonInvalidHost, Message: fmt.Sprintf("spec.githubAPI.host (%q) is not valid: %s", host, reason), } } @@ -354,62 +417,49 @@ func validateHost(gitHubAPIConfig idpv1alpha1.GitHubAPIConfig) (*metav1.Conditio return &metav1.Condition{ Type: HostValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", host), }, &hostPort } -func (c *gitHubWatcherController) validateTLSConfiguration(tlsSpec *idpv1alpha1.TLSSpec) (*metav1.Condition, *x509.CertPool) { - certPool, _, buildCertPoolErr := pinnipedcontroller.BuildCertPoolIDP(tlsSpec) - if buildCertPoolErr != nil { - // buildCertPoolErr is not recoverable with a resync. - // It requires user interaction, so do not return the error. - return &metav1.Condition{ - Type: TLSConfigurationValid, - Status: metav1.ConditionFalse, - Reason: "InvalidTLSConfig", - Message: fmt.Sprintf("spec.githubAPI.tls.certificateAuthorityData is not valid: %s", buildCertPoolErr), - }, nil - } - - return &metav1.Condition{ - Type: TLSConfigurationValid, - Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", - }, certPool -} - func (c *gitHubWatcherController) validateGitHubConnection( hostPort *endpointaddr.HostPort, - certPool *x509.CertPool, - validSoFar bool, + caBundle *tlsconfigutil.CABundle, + hostConditionOk, tlsConfigConditionOk bool, ) (*metav1.Condition, string, *http.Client, error) { - if !validSoFar { + if !hostConditionOk || !tlsConfigConditionOk { return &metav1.Condition{ Type: GitHubConnectionValid, Status: metav1.ConditionUnknown, - Reason: "UnableToValidate", + Reason: conditionsutil.ReasonUnableToValidate, Message: "unable to validate; see other conditions for details", }, "", nil, nil } - conn, tlsDialErr := c.dialFunc("tcp", hostPort.Endpoint(), ptls.Default(certPool)) - if tlsDialErr != nil { - return &metav1.Condition{ - Type: GitHubConnectionValid, - Status: metav1.ConditionFalse, - Reason: "UnableToDialServer", - Message: fmt.Sprintf("cannot dial server spec.githubAPI.host (%q): %s", hostPort.Endpoint(), buildDialErrorMessage(tlsDialErr)), - }, "", nil, tlsDialErr + address := hostPort.Endpoint() + + if !c.validatedCache.IsValid(address, caBundle.Hash()) { + conn, tlsDialErr := c.dialFunc("tcp", address, ptls.Default(caBundle.CertPool())) + if tlsDialErr != nil { + return &metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionFalse, + Reason: conditionsutil.ReasonUnableToDialServer, + Message: fmt.Sprintf("cannot dial server spec.githubAPI.host (%q): %s", address, buildDialErrorMessage(tlsDialErr)), + }, "", nil, tlsDialErr + } + // Any error should be ignored. We have performed a successful Dial, so no need to requeue this Sync. + _ = conn.Close() } + c.validatedCache.MarkAsValidated(address, caBundle.Hash()) + return &metav1.Condition{ Type: GitHubConnectionValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", hostPort.Endpoint()), - }, fmt.Sprintf("https://%s", hostPort.Endpoint()), phttp.Default(certPool), conn.Close() + Reason: conditionsutil.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", address), + }, fmt.Sprintf("https://%s", address), phttp.Default(caBundle.CertPool()), nil } // buildDialErrorMessage standardizes DNS error messages that appear differently on different platforms, so that tests and log grepping is uniform. @@ -432,7 +482,7 @@ func validateUserAndGroupAttributes(upstream *idpv1alpha1.GitHubIdentityProvider return &metav1.Condition{ Type: ClaimsValid, Status: metav1.ConditionFalse, - Reason: "Invalid", + Reason: reasonInvalid, Message: message, } } @@ -471,7 +521,7 @@ func validateUserAndGroupAttributes(upstream *idpv1alpha1.GitHubIdentityProvider return &metav1.Condition{ Type: ClaimsValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.claims are valid", }, groupNameAttribute, usernameAttribute } diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go index 5b9df963f..efaae07b8 100644 --- a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go @@ -7,10 +7,12 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "encoding/base64" "fmt" "net" "net/http" + "slices" "strings" "testing" "time" @@ -22,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/cache" utilnet "k8s.io/apimachinery/pkg/util/net" k8sinformers "k8s.io/client-go/informers" kubernetesfake "k8s.io/client-go/kubernetes/fake" @@ -34,8 +37,9 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" supervisorinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" - pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" "go.pinniped.dev/internal/federationdomain/upstreamprovider" @@ -64,11 +68,15 @@ func TestController(t *testing.T) { }), tlsserver.RecordTLSHello) goodServerDomain, _ := strings.CutPrefix(goodServer.URL, "https://") goodServerCAB64 := base64.StdEncoding.EncodeToString(goodServerCA) + goodServerCertPool := x509.NewCertPool() + goodServerCertPool.AppendCertsFromPEM(goodServerCA) goodServerIPv6, goodServerIPv6CA := tlsserver.TestServerIPv6(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { }), tlsserver.RecordTLSHello) goodServerIPv6Domain, _ := strings.CutPrefix(goodServerIPv6.URL, "https://") goodServerIPv6CAB64 := base64.StdEncoding.EncodeToString(goodServerIPv6CA) + goodServerIPv6CertPool := x509.NewCertPool() + goodServerIPv6CertPool.AppendCertsFromPEM(goodServerCA) caForUnknownServer, err := certauthority.New("Some Unknown CA", time.Hour) require.NoError(t, err) @@ -86,7 +94,7 @@ func TestController(t *testing.T) { frozenClockForLastTransitionTime := clocktesting.NewFakeClock(wantFrozenTime) wantLastTransitionTime := metav1.Time{Time: wantFrozenTime} - goodSecret := &corev1.Secret{ + goodClientCredentialsSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "some-secret-name", Namespace: namespace, @@ -98,6 +106,27 @@ func TestController(t *testing.T) { }, } + goodCABundleSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle-secret-name", + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": goodServerCA, + }, + } + + goodCABundleConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle-secret-name", + Namespace: namespace, + }, + Data: map[string]string{ + "ca.crt": string(goodServerCA), + }, + } + validMinimalIDP := &idpv1alpha1.GitHubIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ Name: "minimal-idp-name", @@ -113,7 +142,7 @@ func TestController(t *testing.T) { }, }, Client: idpv1alpha1.GitHubClientSpec{ - SecretName: goodSecret.Name, + SecretName: goodClientCredentialsSecret.Name, }, // These claims are optional when using the actual Kubernetes CRD. // However, they are required here because CRD defaulting/validation does not occur during testing. @@ -154,7 +183,7 @@ func TestController(t *testing.T) { }, }, Client: idpv1alpha1.GitHubClientSpec{ - SecretName: goodSecret.Name, + SecretName: goodClientCredentialsSecret.Name, }, }, } @@ -167,7 +196,7 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", host), } } @@ -184,8 +213,7 @@ func TestController(t *testing.T) { Message: fmt.Sprintf(`spec.githubAPI.host (%q) is not valid: %s`, host, message), } } - - buildTLSConfigurationValidTrue := func(t *testing.T) metav1.Condition { + buildTLSConfigurationValidTrueWithMsg := func(t *testing.T, msg string) metav1.Condition { t.Helper() return metav1.Condition{ @@ -193,11 +221,16 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + Reason: conditionsutil.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.tls is valid: %s", msg), } } + buildTLSConfigurationValidTrue := func(t *testing.T) metav1.Condition { + t.Helper() + return buildTLSConfigurationValidTrueWithMsg(t, "using configured CA bundle") + } + buildTLSConfigurationValidFalse := func(t *testing.T, message string) metav1.Condition { t.Helper() @@ -219,7 +252,7 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.allowAuthentication.organizations.policy (%q) is valid", policy), } } @@ -245,7 +278,7 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", secretName), } } @@ -276,7 +309,7 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.claims are valid", } } @@ -302,7 +335,7 @@ func TestController(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: wantObservedGeneration, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", host), } } @@ -370,14 +403,16 @@ func TestController(t *testing.T) { } tests := []struct { - name string - githubIdentityProviders []runtime.Object - secrets []runtime.Object - mockDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) - wantErr string - wantLogs []string - wantResultingCache []*upstreamgithub.ProviderConfig - wantResultingUpstreams []idpv1alpha1.GitHubIdentityProvider + name string + githubIdentityProviders []runtime.Object + secretsAndConfigMaps []runtime.Object + mockDialer func(t *testing.T) func(network, addr string, config *tls.Config) (*tls.Conn, error) + preexistingValidatedCache []GitHubValidatedAPICacheKey + wantErr string + wantLogs []string + wantResultingCache []*upstreamgithub.ProviderConfig + wantResultingUpstreams []idpv1alpha1.GitHubIdentityProvider + wantValidatedCache []GitHubValidatedAPICacheKey }{ { name: "no GitHubIdentityProviders", @@ -386,8 +421,8 @@ func TestController(t *testing.T) { wantLogs: []string{}, }, { - name: "happy path with all fields", - secrets: []runtime.Object{goodSecret}, + name: "happy path with all fields", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ validFilledOutIDP, }, @@ -411,7 +446,13 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), }, }, wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ @@ -436,14 +477,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Ready"), }, }, { - name: "happy path with minimal fields", - secrets: []runtime.Object{goodSecret}, + name: "happy path with minimal fields", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ validMinimalIDP, }, @@ -467,7 +508,13 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet(), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), }, }, wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ @@ -492,14 +539,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Ready"), }, }, { - name: "happy path using github.com", - secrets: []runtime.Object{goodSecret}, + name: "happy path using github.com", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { githubIDP := validMinimalIDP.DeepCopy() @@ -508,14 +555,16 @@ func TestController(t *testing.T) { return githubIDP }(), }, - mockDialer: func(network, addr string, config *tls.Config) (*tls.Conn, error) { - require.Equal(t, "github.com:443", addr) - // don't actually dial github.com to avoid making external network calls in unit test - certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(validMinimalIDP.Spec.GitHubAPI.TLS) - require.NoError(t, err) - configClone := config.Clone() - configClone.RootCAs = certPool - return tls.Dial(network, goodServerDomain, configClone) + mockDialer: func(t *testing.T) func(network, addr string, config *tls.Config) (*tls.Conn, error) { + t.Helper() + + return func(network, addr string, config *tls.Config) (*tls.Conn, error) { + require.Equal(t, "github.com:443", addr) + // don't actually dial github.com to avoid making external network calls in unit test + configClone := config.Clone() + configClone.RootCAs = goodServerCertPool + return tls.Dial(network, goodServerDomain, configClone) + } }, wantResultingCache: []*upstreamgithub.ProviderConfig{ { @@ -537,7 +586,13 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet(), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: "github.com:443", + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), }, }, wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ @@ -567,14 +622,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, "github.com"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, "github.com:443"), buildLogForUpdatingPhase("minimal-idp-name", "Ready"), }, }, { - name: "happy path with IPv6", - secrets: []runtime.Object{goodSecret}, + name: "happy path with IPv6", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { ipv6IDP := validMinimalIDP.DeepCopy() @@ -605,7 +660,13 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet(), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerIPv6CertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerIPv6Domain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerIPv6CA), }, }, wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ @@ -638,17 +699,17 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, goodServerIPv6Domain), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, goodServerIPv6Domain), buildLogForUpdatingPhase("minimal-idp-name", "Ready"), }, }, { name: "multiple idps - two good, one invalid", - secrets: []runtime.Object{ - goodSecret, + secretsAndConfigMaps: []runtime.Object{ + goodClientCredentialsSecret, func() runtime.Object { - otherSecret := goodSecret.DeepCopy() + otherSecret := goodClientCredentialsSecret.DeepCopy() otherSecret.Name = "other-secret-name" otherSecret.Data["clientID"] = []byte("other-client-id") otherSecret.Data["clientSecret"] = []byte("other-client-secret") @@ -662,7 +723,7 @@ func TestController(t *testing.T) { otherIDP.Name = "other-idp-name" otherIDP.Spec.Client.SecretName = "other-secret-name" - // No other test happens to that this particular value passes validation + // No other test happens to verify that this particular value passes validation otherIDP.Spec.Claims.Username = ptr.To(idpv1alpha1.GitHubUsernameLoginAndID) return otherIDP }(), @@ -693,7 +754,7 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerCertPool), }, { Name: "other-idp-name", @@ -714,7 +775,13 @@ func TestController(t *testing.T) { Scopes: []string{"read:user", "read:org"}, }, AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), - HttpClient: nil, // let the test runner populate this for us + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), }, }, wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ @@ -792,7 +859,7 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("invalid-idp-name"), buildLogForUpdatingOrganizationPolicyValid("invalid-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("invalid-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("invalid-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("invalid-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("invalid-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("invalid-idp-name", "Error"), @@ -800,7 +867,7 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("other-idp-name"), buildLogForUpdatingOrganizationPolicyValid("other-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("other-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("other-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("other-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("other-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("other-idp-name", "Ready"), @@ -808,14 +875,252 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Ready"), }, }, { - name: "Host error - missing spec.githubAPI.host", - secrets: []runtime.Object{goodSecret}, + name: "happy path for external TLS configuration - one secret and one configmap", + secretsAndConfigMaps: []runtime.Object{ + goodClientCredentialsSecret, + goodCABundleSecret, + goodCABundleConfigMap, + }, + // Note that the order here does not match the order below. + // GitHubIDPs are processed in lexical order. + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + otherIDP := validFilledOutIDP.DeepCopy() + otherIDP.Name = "idp-with-tls-in-secret" + otherIDP.Spec.GitHubAPI.TLS = &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: goodCABundleSecret.Name, + Key: "ca.crt", + }, + } + return otherIDP + }(), + func() runtime.Object { + otherIDP := validFilledOutIDP.DeepCopy() + otherIDP.Name = "idp-with-tls-in-config-map" + otherIDP.Spec.GitHubAPI.TLS = &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: goodCABundleConfigMap.Name, + Key: "ca.crt", + }, + } + return otherIDP + }(), + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "idp-with-tls-in-secret", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: phttp.Default(goodServerCertPool), + }, + { + Name: "idp-with-tls-in-config-map", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), + }, + }, + wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: func() metav1.ObjectMeta { + otherMeta := validFilledOutIDP.ObjectMeta.DeepCopy() + otherMeta.Name = "idp-with-tls-in-config-map" + return *otherMeta + }(), + Spec: func() idpv1alpha1.GitHubIdentityProviderSpec { + otherSpec := validFilledOutIDP.Spec.DeepCopy() + otherSpec.GitHubAPI.TLS = &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: goodCABundleSecret.Name, + Key: "ca.crt", + }, + } + return *otherSpec + }(), + Status: idpv1alpha1.GitHubIdentityProviderStatus{ + Phase: idpv1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, "some-secret-name"), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + { + ObjectMeta: func() metav1.ObjectMeta { + otherMeta := validFilledOutIDP.ObjectMeta.DeepCopy() + otherMeta.Name = "idp-with-tls-in-secret" + return *otherMeta + }(), + Spec: func() idpv1alpha1.GitHubIdentityProviderSpec { + otherSpec := validFilledOutIDP.Spec.DeepCopy() + otherSpec.GitHubAPI.TLS = &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: goodCABundleSecret.Name, + Key: "ca.crt", + }, + } + return *otherSpec + }(), + Status: idpv1alpha1.GitHubIdentityProviderStatus{ + Phase: idpv1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, "some-secret-name"), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("idp-with-tls-in-config-map", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("idp-with-tls-in-config-map"), + buildLogForUpdatingOrganizationPolicyValid("idp-with-tls-in-config-map", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("idp-with-tls-in-config-map", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("idp-with-tls-in-config-map", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), + buildLogForUpdatingGitHubConnectionValid("idp-with-tls-in-config-map", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("idp-with-tls-in-config-map", "Ready"), + + buildLogForUpdatingClientCredentialsSecretValid("idp-with-tls-in-secret", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("idp-with-tls-in-secret"), + buildLogForUpdatingOrganizationPolicyValid("idp-with-tls-in-secret", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("idp-with-tls-in-secret", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("idp-with-tls-in-secret", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), + buildLogForUpdatingGitHubConnectionValid("idp-with-tls-in-secret", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("idp-with-tls-in-secret", "Ready"), + }, + }, + { + name: "happy path with previously validated address/CA Bundle does not validate again", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, + githubIdentityProviders: []runtime.Object{validFilledOutIDP}, + mockDialer: func(t *testing.T) func(network, addr string, config *tls.Config) (*tls.Conn, error) { + t.Helper() + + return func(network, addr string, config *tls.Config) (*tls.Conn, error) { + t.Errorf("this test should not perform dial") + t.FailNow() + return nil, nil + } + }, + preexistingValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), + }, + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "some-idp-name", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: phttp.Default(goodServerCertPool), + }, + }, + wantValidatedCache: []GitHubValidatedAPICacheKey{ + { + address: goodServerDomain, + caBundleHash: tlsconfigutil.NewCABundleHash(goodServerCA), + }, + }, + wantResultingUpstreams: []idpv1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: validFilledOutIDP.Spec, + Status: idpv1alpha1.GitHubIdentityProviderStatus{ + Phase: idpv1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Ready"), + }, + }, + { + name: "Host error - missing spec.githubAPI.host", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -849,14 +1154,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: must not be empty`, ""), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("some-idp-name"), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Host error - protocol/schema is specified", - secrets: []runtime.Object{goodSecret}, + name: "Host error - protocol/schema is specified", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -890,14 +1195,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: invalid port \"//example.com\"`, "https://example.com"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Host error - path is specified", - secrets: []runtime.Object{goodSecret}, + name: "Host error - path is specified", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -931,14 +1236,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com/foo\" is not a valid hostname or IP address`, "example.com/foo"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Host error - userinfo is specified", - secrets: []runtime.Object{goodSecret}, + name: "Host error - userinfo is specified", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -972,14 +1277,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: invalid port \"p@example.com\"`, "u:p@example.com"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Host error - query is specified", - secrets: []runtime.Object{goodSecret}, + name: "Host error - query is specified", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -1013,14 +1318,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com?a=b\" is not a valid hostname or IP address`, "example.com?a=b"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Host error - fragment is specified", - secrets: []runtime.Object{goodSecret}, + name: "Host error - fragment is specified", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -1054,14 +1359,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com#a\" is not a valid hostname or IP address`, "example.com#a"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "TLS error - invalid bundle", - secrets: []runtime.Object{goodSecret}, + name: "TLS error - invalid bundle", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1089,7 +1394,7 @@ func TestController(t *testing.T) { buildGitHubConnectionValidUnknown(t), buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), - buildTLSConfigurationValidFalse(t, "spec.githubAPI.tls.certificateAuthorityData is not valid: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates"), + buildTLSConfigurationValidFalse(t, `spec.githubAPI.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 4 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`), }, }, }, @@ -1099,14 +1404,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "False", "InvalidTLSConfig", "spec.githubAPI.tls.certificateAuthorityData is not valid: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "False", "InvalidTLSConfig", `spec.githubAPI.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 4 bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")`), buildLogForUpdatingGitHubConnectionValidUnknown("some-idp-name"), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Connection error - no such host", - secrets: []runtime.Object{goodSecret}, + name: "Connection error - no such host", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -1141,14 +1446,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, "nowhere.bad-tld"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): dial tcp: lookup nowhere.bad-tld: no such host`, "nowhere.bad-tld:443"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Connection error - ipv6 without brackets", - secrets: []runtime.Object{goodSecret}, + name: "Connection error - ipv6 without brackets", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validMinimalIDP.DeepCopy() @@ -1182,14 +1487,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"0:0:0:0:0:0:0:1:9876\") is not valid: host \"%s\" is not a valid hostname or IP address`, "0:0:0:0:0:0:0:1:9876"), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { - name: "Connection error - host not trusted by system certs", - secrets: []runtime.Object{goodSecret}, + name: "Connection error - host not trusted by system certs", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1214,7 +1519,7 @@ func TestController(t *testing.T) { buildGitHubConnectionValidFalse(t, fmt.Sprintf(`cannot dial server spec.githubAPI.host (%q): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host)), buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), - buildTLSConfigurationValidTrue(t), + buildTLSConfigurationValidTrueWithMsg(t, "no TLS configuration provided: using default root CA bundle from container image"), }, }, }, @@ -1224,14 +1529,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Connection error - host not trusted by provided CA bundle", - secrets: []runtime.Object{goodSecret}, + name: "Connection error - host not trusted by provided CA bundle", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1270,14 +1575,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Organization Policy error - missing spec.allowAuthentication.organizations.policy", - secrets: []runtime.Object{goodSecret}, + name: "Organization Policy error - missing spec.allowAuthentication.organizations.policy", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1311,14 +1616,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Organization Policy error - invalid spec.allowAuthentication.organizations.policy", - secrets: []runtime.Object{goodSecret}, + name: "Organization Policy error - invalid spec.allowAuthentication.organizations.policy", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1352,14 +1657,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", - secrets: []runtime.Object{goodSecret}, + name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1393,14 +1698,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", - secrets: []runtime.Object{goodSecret}, + name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1434,14 +1739,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("some-idp-name"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty"), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Invalid Claims - missing spec.claims.username", - secrets: []runtime.Object{goodSecret}, + name: "Invalid Claims - missing spec.claims.username", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1475,14 +1780,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidFalse("some-idp-name", "spec.claims.username is required"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Invalid Claims - invalid spec.claims.username", - secrets: []runtime.Object{goodSecret}, + name: "Invalid Claims - invalid spec.claims.username", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1516,14 +1821,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidFalse("some-idp-name", `spec.claims.username (\"a\") is not valid`), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Invalid Claims - missing spec.claims.groups", - secrets: []runtime.Object{goodSecret}, + name: "Invalid Claims - missing spec.claims.groups", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1557,14 +1862,14 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidFalse("some-idp-name", "spec.claims.groups is required"), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { - name: "Invalid Claims - invalid spec.claims.groups", - secrets: []runtime.Object{goodSecret}, + name: "Invalid Claims - invalid spec.claims.groups", + secretsAndConfigMaps: []runtime.Object{goodClientCredentialsSecret}, githubIdentityProviders: []runtime.Object{ func() runtime.Object { badIDP := validFilledOutIDP.DeepCopy() @@ -1598,16 +1903,16 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidFalse("some-idp-name", `spec.claims.groups (\"b\") is not valid`), buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("some-idp-name", "Error"), }, }, { name: "Client Secret error - in different namespace", - secrets: []runtime.Object{ + secretsAndConfigMaps: []runtime.Object{ func() runtime.Object { - badSecret := goodSecret.DeepCopy() + badSecret := goodClientCredentialsSecret.DeepCopy() badSecret.Namespace = "other-namespace" return badSecret }(), @@ -1641,16 +1946,16 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { name: "Client Secret error - wrong type", - secrets: []runtime.Object{ + secretsAndConfigMaps: []runtime.Object{ func() runtime.Object { - badSecret := goodSecret.DeepCopy() + badSecret := goodClientCredentialsSecret.DeepCopy() badSecret.Type = "other-type" return badSecret }(), @@ -1684,16 +1989,16 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { name: "Client Secret error - missing clientId", - secrets: []runtime.Object{ + secretsAndConfigMaps: []runtime.Object{ func() runtime.Object { - badSecret := goodSecret.DeepCopy() + badSecret := goodClientCredentialsSecret.DeepCopy() delete(badSecret.Data, "clientID") return badSecret }(), @@ -1727,16 +2032,16 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { name: "Client Secret error - missing clientSecret", - secrets: []runtime.Object{ + secretsAndConfigMaps: []runtime.Object{ func() runtime.Object { - badSecret := goodSecret.DeepCopy() + badSecret := goodClientCredentialsSecret.DeepCopy() delete(badSecret.Data, "clientSecret") return badSecret }(), @@ -1770,16 +2075,16 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, }, { name: "Client Secret error - additional data", - secrets: []runtime.Object{ + secretsAndConfigMaps: []runtime.Object{ func() runtime.Object { - badSecret := goodSecret.DeepCopy() + badSecret := goodClientCredentialsSecret.DeepCopy() badSecret.Data["foo"] = []byte("bar") return badSecret }(), @@ -1813,7 +2118,7 @@ func TestController(t *testing.T) { buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), - buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls is valid: using configured CA bundle"), buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), buildLogForUpdatingPhase("minimal-idp-name", "Error"), }, @@ -1827,11 +2132,11 @@ func TestController(t *testing.T) { fakeSupervisorClient := supervisorfake.NewSimpleClientset(tt.githubIdentityProviders...) supervisorInformers := supervisorinformers.NewSharedInformerFactory(fakeSupervisorClient, 0) - fakeKubeClient := kubernetesfake.NewSimpleClientset(tt.secrets...) + fakeKubeClient := kubernetesfake.NewSimpleClientset(tt.secretsAndConfigMaps...) kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(fakeKubeClient, 0) - cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() - cache.SetGitHubIdentityProviders([]upstreamprovider.UpstreamGithubIdentityProviderI{ + idpCache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() + idpCache.SetGitHubIdentityProviders([]upstreamprovider.UpstreamGithubIdentityProviderI{ upstreamgithub.New( upstreamgithub.ProviderConfig{Name: "initial-entry-to-remove"}, ), @@ -1842,21 +2147,28 @@ func TestController(t *testing.T) { gitHubIdentityProviderInformer := supervisorInformers.IDP().V1alpha1().GitHubIdentityProviders() - dialer := tt.mockDialer - if dialer == nil { - dialer = tls.Dial + dialer := tls.Dial + if tt.mockDialer != nil { + dialer = tt.mockDialer(t) + } + + validatedCache := cache.NewExpiring() + for _, item := range tt.preexistingValidatedCache { + validatedCache.Set(item, nil, 1*time.Hour) } controller := New( namespace, - cache, + idpCache, fakeSupervisorClient, gitHubIdentityProviderInformer, kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), logger, controllerlib.WithInformer, frozenClockForLastTransitionTime, dialer, + validatedCache, ) ctx, cancel := context.WithCancel(context.Background()) @@ -1875,8 +2187,8 @@ func TestController(t *testing.T) { require.NoError(t, err) } - // Verify what's in the cache - actualIDPList := cache.GetGitHubIdentityProviders() + // Verify what's in the IDP cache + actualIDPList := idpCache.GetGitHubIdentityProviders() require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range len(tt.wantResultingCache) { // Do not expect any particular order in the cache @@ -1897,17 +2209,24 @@ func TestController(t *testing.T) { require.Equal(t, tt.wantResultingCache[i].UsernameAttribute, actualProvider.GetUsernameAttribute()) require.Equal(t, tt.wantResultingCache[i].AllowedOrganizations, actualProvider.GetAllowedOrganizations()) - require.GreaterOrEqual(t, len(tt.githubIdentityProviders), i+1, "there must be at least as many input identity providers as items in the cache") - githubIDP, ok := tt.githubIdentityProviders[i].(*idpv1alpha1.GitHubIdentityProvider) - require.True(t, ok) - certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(githubIDP.Spec.GitHubAPI.TLS) - require.NoError(t, err) - - compareTLSClientConfigWithinHttpClients(t, phttp.Default(certPool), actualProvider.GetConfig().HttpClient) + compareTLSClientConfigWithinHttpClients(t, tt.wantResultingCache[i].HttpClient, actualProvider.GetConfig().HttpClient) require.Equal(t, tt.wantResultingCache[i].OAuth2Config, actualProvider.GetConfig().OAuth2Config) require.Contains(t, tt.wantResultingCache[i].APIBaseURL, actualProvider.GetConfig().APIBaseURL) } + // Verify what's in the validated cache + var uniqueAddresses []string + for _, cachedIDP := range tt.wantResultingCache { + if !slices.Contains(uniqueAddresses, cachedIDP.APIBaseURL) { + uniqueAddresses = append(uniqueAddresses, cachedIDP.APIBaseURL) + } + } + require.Equal(t, len(uniqueAddresses), len(tt.wantValidatedCache), "every unique IDP address should have an entry in the validated cache") + for _, item := range tt.wantValidatedCache { + _, ok := validatedCache.Get(item) + require.True(t, ok, "item with address %q must be found in the validated cache", item.address) + } + // Verify the status conditions as reported in Kubernetes allGitHubIDPs, err := fakeSupervisorClient.IDPV1alpha1().GitHubIdentityProviders(namespace).List(ctx, metav1.ListOptions{}) require.NoError(t, err) @@ -2058,7 +2377,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 333, LastTransitionTime: oneHourAgo, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", goodServerDomain), }, { @@ -2066,7 +2385,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 333, LastTransitionTime: oneHourAgo, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", goodServerDomain), }, { @@ -2074,7 +2393,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 333, LastTransitionTime: oneHourAgo, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, }, { @@ -2082,8 +2401,8 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 333, LastTransitionTime: oneHourAgo, - Reason: upstreamwatchers.ReasonSuccess, - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + Reason: conditionsutil.ReasonSuccess, + Message: "spec.githubAPI.tls is valid: using configured CA bundle", }, }, }, @@ -2154,7 +2473,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "spec.claims are valid", }, { @@ -2162,7 +2481,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: `clientID and clientSecret have been read from spec.client.SecretName ("some-secret-name")`, }, { @@ -2170,7 +2489,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", goodServerDomain), }, { @@ -2178,7 +2497,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", goodServerDomain), }, { @@ -2186,7 +2505,7 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, }, { @@ -2194,8 +2513,8 @@ func TestController_OnlyWantActions(t *testing.T) { Status: metav1.ConditionTrue, ObservedGeneration: 1234, LastTransitionTime: wantLastTransitionTime, - Reason: upstreamwatchers.ReasonSuccess, - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + Reason: conditionsutil.ReasonSuccess, + Message: "spec.githubAPI.tls is valid: using configured CA bundle", }, }, } @@ -2227,10 +2546,12 @@ func TestController_OnlyWantActions(t *testing.T) { fakeSupervisorClient, supervisorInformers.IDP().V1alpha1().GitHubIdentityProviders(), kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), logger, controllerlib.WithInformer, frozenClockForLastTransitionTime, tls.Dial, + cache.NewExpiring(), ) ctx, cancel := context.WithCancel(context.Background()) @@ -2273,12 +2594,10 @@ func compareTLSClientConfigWithinHttpClients(t *testing.T, expected *http.Client } func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { - namespace := "some-namespace" goodSecret := &corev1.Secret{ Type: "secrets.pinniped.dev/github-client", ObjectMeta: metav1.ObjectMeta{ - Name: "some-name", - Namespace: namespace, + Name: "some-name", }, } @@ -2290,22 +2609,36 @@ func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { wantDelete bool }{ { - name: "a secret of the right type", + name: "should return true for a secret of the type secrets.pinniped.dev/github-client", secret: goodSecret, wantAdd: true, wantUpdate: true, wantDelete: true, }, { - name: "a secret of the right type, but in the wrong namespace", + name: "should return true for a secret of the type Opaque", secret: func() *corev1.Secret { otherSecret := goodSecret.DeepCopy() - otherSecret.Namespace = "other-namespace" + otherSecret.Type = corev1.SecretTypeOpaque return otherSecret }(), + wantAdd: true, + wantUpdate: true, + wantDelete: true, }, { - name: "a secret of the wrong type", + name: "should return true for a secret of the type TLS", + secret: func() *corev1.Secret { + otherSecret := goodSecret.DeepCopy() + otherSecret.Type = corev1.SecretTypeTLS + return otherSecret + }(), + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", secret: func() *corev1.Secret { otherSecret := goodSecret.DeepCopy() otherSecret.Type = "other-type" @@ -2313,7 +2646,7 @@ func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { }(), }, { - name: "resource of wrong data type", + name: "should return false for a resource of wrong data type", secret: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, @@ -2328,19 +2661,21 @@ func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { var log bytes.Buffer logger := plog.TestLogger(t, &log) - secretInformer := kubeInformers.Core().V1().Secrets() observableInformers := testutil.NewObservableWithInformerOption() + secretInformer := kubeInformers.Core().V1().Secrets() _ = New( - namespace, + "some-namespace", dynamicupstreamprovider.NewDynamicUpstreamIDPProvider(), supervisorfake.NewSimpleClientset(), supervisorinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders(), secretInformer, + kubeInformers.Core().V1().ConfigMaps(), logger, observableInformers.WithInformer, clock.RealClock{}, tls.Dial, + cache.NewExpiring(), ) unrelated := &corev1.Secret{} @@ -2353,6 +2688,63 @@ func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { } } +func TestGitHubUpstreamWatcherControllerFilterConfigMaps(t *testing.T) { + namespace := "some-namespace" + goodCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any ConfigMap", + cm: goodCM, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + observableInformers := testutil.NewObservableWithInformerOption() + configMapInformer := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0).Core().V1().ConfigMaps() + + _ = New( + namespace, + dynamicupstreamprovider.NewDynamicUpstreamIDPProvider(), + supervisorfake.NewSimpleClientset(), + supervisorinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders(), + k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0).Core().V1().Secrets(), + configMapInformer, + logger, + observableInformers.WithInformer, + clock.RealClock{}, + tls.Dial, + cache.NewExpiring(), + ) + + unrelated := &corev1.ConfigMap{} + filter := observableInformers.GetFilterForInformer(configMapInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.cm)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.cm, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.cm)) + }) + } +} + func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) { namespace := "some-namespace" goodIDP := &idpv1alpha1.GitHubIdentityProvider{ @@ -2369,26 +2761,12 @@ func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) { wantDelete bool }{ { - name: "an IDP in the right namespace", + name: "any GitHubIdentityProvider", idp: goodIDP, wantAdd: true, wantUpdate: true, wantDelete: true, }, - { - name: "IDP in the wrong namespace", - idp: func() metav1.Object { - badIDP := goodIDP.DeepCopy() - badIDP.Namespace = "other-namespace" - return badIDP - }(), - }, - { - name: "resource of wrong data type", - idp: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: "some-name"}, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2397,8 +2775,8 @@ func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) { var log bytes.Buffer logger := plog.TestLogger(t, &log) - gitHubIdentityProviderInformer := supervisorinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders() observableInformers := testutil.NewObservableWithInformerOption() + gitHubIdentityProviderInformer := supervisorinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders() _ = New( namespace, @@ -2406,10 +2784,12 @@ func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) { supervisorfake.NewSimpleClientset(), gitHubIdentityProviderInformer, k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0).Core().V1().Secrets(), + k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0).Core().V1().ConfigMaps(), logger, observableInformers.WithInformer, clock.RealClock{}, tls.Dial, + cache.NewExpiring(), ) unrelated := &idpv1alpha1.GitHubIdentityProvider{} diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 820de4b48..a34b6992b 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -8,6 +8,7 @@ import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -143,6 +144,7 @@ type ldapWatcherController struct { client supervisorclientset.Interface ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer secretInformer corev1informers.SecretInformer + configMapInformer corev1informers.ConfigMapInformer } // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. @@ -151,6 +153,7 @@ func New( client supervisorclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return newInternal( @@ -162,6 +165,7 @@ func New( client, ldapIdentityProviderInformer, secretInformer, + configMapInformer, withInformer, ) } @@ -174,6 +178,7 @@ func newInternal( client supervisorclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { c := ldapWatcherController{ @@ -183,6 +188,7 @@ func newInternal( client: client, ldapIdentityProviderInformer: ldapIdentityProviderInformer, secretInformer: secretInformer, + configMapInformer: configMapInformer, } return controllerlib.New( controllerlib.Config{Name: ldapControllerName, Syncer: &c}, @@ -193,7 +199,17 @@ func newInternal( ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + upstreamwatchers.LDAPBindAccountSecretType, + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) @@ -249,7 +265,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * Dialer: c.ldapDialer, } - conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSettingsCache, config) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.configMapInformer, c.validatedSettingsCache, config) c.updateStatus(ctx, upstream, conditions.Conditions()) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index c5f313d4a..f529e9182 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -26,6 +26,7 @@ import ( supervisorinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" @@ -46,7 +47,7 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { wantDelete bool }{ { - name: "a secret of the right type", + name: "should return true for a secret of type BasicAuth", secret: &corev1.Secret{ Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, @@ -56,14 +57,34 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { wantDelete: true, }, { - name: "a secret of the wrong type", + name: "should return true for a secret of type Opaque", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return true for a secret of type TLS", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", secret: &corev1.Secret{ Type: "this-is-the-wrong-type", ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, }, { - name: "resource of a data type which is not watched by this controller", + name: "should return false for a resource of a data type which is not watched by this controller", secret: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, @@ -79,9 +100,10 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() withInformer := testutil.NewObservableWithInformerOption() - New(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, ldapIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(secretInformer) @@ -93,6 +115,51 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { } } +func TestLDAPUpstreamWatcherControllerFilterConfigMaps(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any configmap", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fakePinnipedClient := supervisorfake.NewSimpleClientset() + pinnipedInformers := supervisorinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + ldapIDPInformer := pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + withInformer := testutil.NewObservableWithInformerOption() + + New(nil, nil, ldapIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) + + unrelated := corev1.ConfigMap{} + filter := withInformer.GetFilterForInformer(configMapInformer) + require.Equal(t, test.wantAdd, filter.Add(test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(test.cm, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(test.cm)) + }) + } +} + func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) { t.Parallel() @@ -123,9 +190,10 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() withInformer := testutil.NewObservableWithInformerOption() - New(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, ldapIDPInformer, secretInformer, configMapInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(ldapIDPInformer) @@ -166,6 +234,9 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testGroupSearchFilter = "test-group-search-filter" testGroupSearchUserAttributeForFilter = "test-group-search-filter-user-attr-for-filter" testGroupSearchNameAttrName = "test-group-name-attr" + + caBundleConfigMapName = "test-ca-bundle-cm" + caBundleSecretName = "test-ca-bundle-secret" //nolint:gosec // this is not a credential ) testValidSecretData := map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} @@ -211,6 +282,50 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return deepCopy } + validUpstreamWithConfigMapCABundleSource := validUpstream.DeepCopy() + validUpstreamWithConfigMapCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithConfigMapCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caBundleConfigMapName, + Key: "ca.crt", + } + caBundleConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleConfigMapName, Namespace: testNamespace}, + Data: map[string]string{ + "ca.crt": string(testCABundle), + }, + } + + validUpstreamWithOpaqueSecretCABundleSource := validUpstream.DeepCopy() + validUpstreamWithOpaqueSecretCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithOpaqueSecretCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caBundleSecretName, + Key: "ca.crt", + } + caBundleOpaqueSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": testCABundle, + }, + } + + validUpstreamWithTLSSecretCABundleSource := validUpstream.DeepCopy() + validUpstreamWithTLSSecretCABundleSource.Spec.TLS.CertificateAuthorityData = "" + validUpstreamWithTLSSecretCABundleSource.Spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caBundleSecretName, + Key: "ca.crt", + } + caBundleTLSSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: caBundleSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": testCABundle, + }, + } + providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ Name: testName, ResourceUID: testResourceUID, @@ -268,13 +383,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { condPtr := func(c metav1.Condition) *metav1.Condition { return &c } - tlsConfigurationValidLoadedTrueCondition := func(gen int64) metav1.Condition { + tlsConfigurationValidLoadedTrueCondition := func(gen int64, msg string) metav1.Condition { return metav1.Condition{ Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "loaded TLS configuration", + Message: fmt.Sprintf("spec.tls is valid: %s", msg), ObservedGeneration: gen, } } @@ -282,7 +397,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return []metav1.Condition{ bindSecretValidTrueCondition(gen), ldapConnectionValidTrueCondition(gen, secretVersion), - tlsConfigurationValidLoadedTrueCondition(gen), + tlsConfigurationValidLoadedTrueCondition(gen, "using configured CA bundle"), } } @@ -310,6 +425,123 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { name: "no LDAPIdentityProvider upstreams clears the cache", wantResultingCache: []*upstreamldap.ProviderConfig{}, }, + { + name: "one valid upstream using a configmap to source CA bundles updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{validUpstreamWithConfigMapCABundleSource}, + inputSecrets: []runtime.Object{validBindUserSecret("4242"), caBundleConfigMap}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID}, + Status: idpv1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + }}, + }, + { + name: "valid upstream spec using a configmap to source CA bundles that is already in the cache is updated to have a new ca bundle: Sync should now update the cache with the new CA bundle hash", + inputUpstreams: []runtime.Object{validUpstreamWithConfigMapCABundleSource}, + inputSecrets: []runtime.Object{validBindUserSecret("4242"), caBundleConfigMap}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash([]byte("this CA bundle should be replaced")), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID}, + Status: idpv1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + }}, + }, + { + name: "one valid upstream using an opaque secret to source CA bundles updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{validUpstreamWithOpaqueSecretCABundleSource}, + inputSecrets: []runtime.Object{validBindUserSecret("4242"), caBundleOpaqueSecret}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID}, + Status: idpv1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + }}, + }, + { + name: "one valid upstream using a TLS secret to source CA bundles updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{validUpstreamWithTLSSecretCABundleSource}, + inputSecrets: []runtime.Object{validBindUserSecret("4242"), caBundleTLSSecret}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []idpv1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID}, + Status: idpv1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), + IDPSpecGeneration: 1234, + ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), + }}, + }, { name: "one valid upstream updates the cache to include only that upstream", inputUpstreams: []runtime.Object{validUpstream}, @@ -332,6 +564,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -355,7 +588,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -383,7 +616,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -410,7 +643,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testBindSecretName), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -434,7 +667,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", - Message: "certificateAuthorityData is invalid: illegal base64 data at input byte 4", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4", ObservedGeneration: 1234, }, }, @@ -460,7 +693,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", - Message: "certificateAuthorityData is invalid: no certificates found", + Message: `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, ObservedGeneration: 1234, }, }, @@ -513,7 +746,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "no TLS configuration provided", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", ObservedGeneration: 1234, }, }, @@ -524,6 +757,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(nil), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -582,7 +816,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { "ldap.example.com", testBindUsername, testBindSecretName, "4242"), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -591,6 +825,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.StartTLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: &metav1.Condition{ Type: "LDAPConnectionValid", @@ -655,7 +890,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { "ldap.example.com:5678", testBindUsername, "ldap.example.com:5678"), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -698,8 +933,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingUpstreams: []idpv1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID}, Status: idpv1alpha1.LDAPIdentityProviderStatus{ - Phase: "Ready", - Conditions: allConditionsTrue(1234, "4242"), + Phase: "Ready", + Conditions: []metav1.Condition{ + bindSecretValidTrueCondition(1234), + ldapConnectionValidTrueCondition(1234, "4242"), + tlsConfigurationValidLoadedTrueCondition(1234, "no TLS configuration provided: using default root CA bundle from container image"), + }, }, }}, wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { @@ -707,6 +946,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(nil), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -741,7 +981,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, "non-existent-secret"), ObservedGeneration: 42, }, - tlsConfigurationValidLoadedTrueCondition(42), + tlsConfigurationValidLoadedTrueCondition(42, "using configured CA bundle"), }, }, }, @@ -758,6 +998,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -790,7 +1031,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, - tlsConfigurationValidLoadedTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234, "using configured CA bundle"), }, }, }}, @@ -810,6 +1051,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -829,6 +1071,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -847,6 +1090,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.StartTLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithStartTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -866,6 +1110,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.StartTLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithStartTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -883,6 +1128,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, IDPSpecGeneration: 1233, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, }}, @@ -904,6 +1150,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -923,6 +1170,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { IDPSpecGeneration: 1234, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), // already previously validated with version 4242 }}, setupMocks: func(conn *mockldapconn.MockConn) { @@ -942,6 +1190,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -980,6 +1229,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -1023,6 +1273,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -1061,6 +1312,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(providerConfigForValidUpstreamWithTLS.CABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}}, @@ -1111,7 +1363,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: "True", LastTransitionTime: now, Reason: "Success", - Message: "loaded TLS configuration", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234, }, }, @@ -1122,6 +1374,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase, + CABundleHash: tlsconfigutil.NewCABundleHash(testCABundle), IDPSpecGeneration: 1234, ConnectionValidCondition: condPtr(ldapConnectionValidTrueConditionWithoutTimeOrGeneration("4242")), }}, @@ -1177,6 +1430,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), controllerlib.WithInformer, ) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index cda75c49f..02ef033e2 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -7,7 +7,6 @@ package oidcupstreamwatcher import ( "context" "crypto/x509" - "encoding/base64" "fmt" "net/http" "net/url" @@ -32,6 +31,7 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/net/phttp" @@ -93,33 +93,44 @@ type UpstreamOIDCIdentityProviderICache interface { SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI) } -// lruValidatorCache caches the *coreosoidc.Provider associated with a particular issuer/TLS configuration. -type lruValidatorCache struct{ cache *cache.Expiring } +// oidcDiscoveryCacheKey is the type of keys in an oidcDiscoveryCache. +type oidcDiscoveryCacheKey struct { + issuer string + caBundleHash tlsconfigutil.CABundleHash +} -type lruValidatorCacheEntry struct { +// oidcDiscoveryCacheValue is the type of cache entries in an oidcDiscoveryCache. +type oidcDiscoveryCacheValue struct { provider *coreosoidc.Provider client *http.Client } -func (c *lruValidatorCache) getProvider(spec *idpv1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.Provider, *http.Client) { - if result, ok := c.cache.Get(c.cacheKey(spec)); ok { - entry := result.(*lruValidatorCacheEntry) - return entry.provider, entry.client - } - return nil, nil +// oidcDiscoveryCache caches the discovered provider along with the http Client to use for making calls to that provider, +// for a particular combination OIDC issuer and CA bundle for that issuer. +type oidcDiscoveryCache interface { + getProvider(oidcDiscoveryCacheKey) *oidcDiscoveryCacheValue + putProvider(oidcDiscoveryCacheKey, *oidcDiscoveryCacheValue) } -func (c *lruValidatorCache) putProvider(spec *idpv1alpha1.OIDCIdentityProviderSpec, provider *coreosoidc.Provider, client *http.Client) { - c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, oidcValidatorCacheTTL) +// ttlProviderCache caches the *coreosoidc.Provider associated with a particular issuer/TLS configuration, +// for a limited time (TTL). +type ttlProviderCache struct{ cache *cache.Expiring } + +// ttlProviderCache implements the oidcDiscoveryCache interface. +var _ oidcDiscoveryCache = (*ttlProviderCache)(nil) + +// getProvider gets an entry from the ttlProviderCache. +func (c *ttlProviderCache) getProvider(key oidcDiscoveryCacheKey) *oidcDiscoveryCacheValue { + if result, ok := c.cache.Get(key); ok { + entry := result.(*oidcDiscoveryCacheValue) + return entry + } + return nil } -func (c *lruValidatorCache) cacheKey(spec *idpv1alpha1.OIDCIdentityProviderSpec) any { - var key struct{ issuer, caBundle string } - key.issuer = spec.Issuer - if spec.TLS != nil { - key.caBundle = spec.TLS.CertificateAuthorityData - } - return key +// putProvider adds to the ttlProviderCache for a limited period of time. +func (c *ttlProviderCache) putProvider(key oidcDiscoveryCacheKey, value *oidcDiscoveryCacheValue) { + c.cache.Set(key, value, oidcValidatorCacheTTL) } type oidcWatcherController struct { @@ -128,10 +139,8 @@ type oidcWatcherController struct { client supervisorclientset.Interface oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer secretInformer corev1informers.SecretInformer - validatorCache interface { - getProvider(*idpv1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.Provider, *http.Client) - putProvider(*idpv1alpha1.OIDCIdentityProviderSpec, *coreosoidc.Provider, *http.Client) - } + configMapInformer corev1informers.ConfigMapInformer + validatorCache oidcDiscoveryCache } // New instantiates a new controllerlib.Controller which will populate the provided UpstreamOIDCIdentityProviderICache. @@ -140,8 +149,10 @@ func New( client supervisorclientset.Interface, oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, log plog.Logger, withInformer pinnipedcontroller.WithInformerOptionFunc, + validatorCache *cache.Expiring, ) controllerlib.Controller { c := oidcWatcherController{ cache: idpCache, @@ -149,7 +160,8 @@ func New( client: client, oidcIdentityProviderInformer: oidcIdentityProviderInformer, secretInformer: secretInformer, - validatorCache: &lruValidatorCache{cache: cache.NewExpiring()}, + configMapInformer: configMapInformer, + validatorCache: &ttlProviderCache{cache: validatorCache}, } return controllerlib.New( controllerlib.Config{Name: oidcControllerName, Syncer: &c}, @@ -160,7 +172,18 @@ func New( ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(oidcClientSecretType, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnySecretOfTypesFilter( + []corev1.SecretType{ + oidcClientSecretType, + corev1.SecretTypeOpaque, + corev1.SecretTypeTLS, + }, + pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + configMapInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) @@ -220,8 +243,9 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst conditions := []*metav1.Condition{ c.validateSecret(upstream, &result), - c.validateIssuer(ctx.Context, upstream, &result), } + conditions = append(conditions, c.validateIssuer(ctx.Context, upstream, &result)...) + if len(rejectedAuthcodeAuthorizeParameters) > 0 { conditions = append(conditions, &metav1.Condition{ Type: typeAdditionalAuthorizeParametersValid, @@ -234,7 +258,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst conditions = append(conditions, &metav1.Condition{ Type: typeAdditionalAuthorizeParametersValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: allParamNamesAllowedMsg, }) } @@ -302,34 +326,61 @@ func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentit return &metav1.Condition{ Type: typeClientCredentialsSecretValid, Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "loaded client credentials", } } // validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition. -func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *idpv1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *metav1.Condition { - // Get the provider and HTTP Client from cache if possible. - discoveredProvider, httpClient := c.validatorCache.getProvider(&upstream.Spec) +func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *idpv1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) []*metav1.Condition { + tlsCondition, caBundle := tlsconfigutil.ValidateTLSConfig( + tlsconfigutil.TLSSpecForSupervisor(upstream.Spec.TLS), + "spec.tls", + upstream.Namespace, + c.secretInformer, + c.configMapInformer) + + // When the TLS config is invalid, return some error conditions. + if tlsCondition.Reason != conditionsutil.ReasonSuccess { + return []*metav1.Condition{ + { + Type: typeOIDCDiscoverySucceeded, + Status: metav1.ConditionFalse, + Reason: tlsconfigutil.ReasonInvalidTLSConfig, + Message: tlsCondition.Message, + }, + tlsCondition, + } + } + + var discoveredProvider *coreosoidc.Provider + var httpClient *http.Client + + // Get the discovered provider and HTTP client from cache, if they are found in the cache. + cacheKey := oidcDiscoveryCacheKey{ + issuer: upstream.Spec.Issuer, + caBundleHash: caBundle.Hash(), + } + if cacheEntry := c.validatorCache.getProvider(cacheKey); cacheEntry != nil { + discoveredProvider = cacheEntry.provider + httpClient = cacheEntry.client + c.log.WithValues( + "namespace", upstream.Namespace, + "name", upstream.Name, + "issuer", upstream.Spec.Issuer, + ).Debug("found previous OIDC discovery result in cache") + } // If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache. if discoveredProvider == nil { - var err error - httpClient, err = getClient(upstream) - if err != nil { - return &metav1.Condition{ - Type: typeOIDCDiscoverySucceeded, - Status: metav1.ConditionFalse, - Reason: upstreamwatchers.ReasonInvalidTLSConfig, - Message: err.Error(), - } - } + httpClient = defaultClientShortTimeout(caBundle.CertPool()) _, issuerURLCondition := validateHTTPSURL(upstream.Spec.Issuer, "issuer", reasonUnreachable) if issuerURLCondition != nil { - return issuerURLCondition + return []*metav1.Condition{issuerURLCondition, tlsCondition} } + var err error discoveredProvider, err = coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer) if err != nil { c.log.WithValues( @@ -337,16 +388,20 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *id "name", upstream.Name, "issuer", upstream.Spec.Issuer, ).Error("failed to perform OIDC discovery", err) - return &metav1.Condition{ - Type: typeOIDCDiscoverySucceeded, - Status: metav1.ConditionFalse, - Reason: reasonUnreachable, - Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)), + return []*metav1.Condition{ + { + Type: typeOIDCDiscoverySucceeded, + Status: metav1.ConditionFalse, + Reason: reasonUnreachable, + Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", + upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)), + }, + tlsCondition, } } // Update the cache with the newly discovered value. - c.validatorCache.putProvider(&upstream.Spec, discoveredProvider, httpClient) + c.validatorCache.putProvider(cacheKey, &oidcDiscoveryCacheValue{provider: discoveredProvider, client: httpClient}) } // Get the revocation endpoint, if there is one. Many providers do not offer a revocation endpoint. @@ -356,11 +411,14 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *id } if err := discoveredProvider.Claims(&additionalDiscoveryClaims); err != nil { // This shouldn't actually happen because the above call to NewProvider() would have already returned this error. - return &metav1.Condition{ - Type: typeOIDCDiscoverySucceeded, - Status: metav1.ConditionFalse, - Reason: reasonInvalidResponse, - Message: fmt.Sprintf("failed to unmarshal OIDC discovery response from %q:\n%s", upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)), + return []*metav1.Condition{ + { + Type: typeOIDCDiscoverySucceeded, + Status: metav1.ConditionFalse, + Reason: reasonInvalidResponse, + Message: fmt.Sprintf("failed to unmarshal OIDC discovery response from %q:\n%s", upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)), + }, + tlsCondition, } } if additionalDiscoveryClaims.RevocationEndpoint != "" { @@ -371,7 +429,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *id reasonInvalidResponse, ) if revocationURLCondition != nil { - return revocationURLCondition + return []*metav1.Condition{revocationURLCondition, tlsCondition} } // Remember the URL for later use. result.RevocationURL = revocationURL @@ -383,7 +441,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *id reasonInvalidResponse, ) if authorizeURLCondition != nil { - return authorizeURLCondition + return []*metav1.Condition{authorizeURLCondition, tlsCondition} } _, tokenURLCondition := validateHTTPSURL( @@ -392,18 +450,21 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *id reasonInvalidResponse, ) if tokenURLCondition != nil { - return tokenURLCondition + return []*metav1.Condition{tokenURLCondition, tlsCondition} } // If everything is valid, update the result and set the condition to true. result.Config.Endpoint = discoveredProvider.Endpoint() result.Provider = discoveredProvider result.Client = httpClient - return &metav1.Condition{ - Type: typeOIDCDiscoverySucceeded, - Status: metav1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: "discovered issuer configuration", + return []*metav1.Condition{ + { + Type: typeOIDCDiscoverySucceeded, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "discovered issuer configuration", + }, + tlsCondition, } } @@ -431,24 +492,6 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *idpv } } -func getClient(upstream *idpv1alpha1.OIDCIdentityProvider) (*http.Client, error) { - if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" { - return defaultClientShortTimeout(nil), nil - } - - bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData) - if err != nil { - return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err) - } - - rootCAs := x509.NewCertPool() - if !rootCAs.AppendCertsFromPEM(bundle) { - return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", upstreamwatchers.ErrNoCertificates) - } - - return defaultClientShortTimeout(rootCAs), nil -} - func defaultClientShortTimeout(rootCAs *x509.CertPool) *http.Client { c := phttp.Default(rootCAs) c.Timeout = time.Minute diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 1f8889434..659e25e8c 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -6,6 +6,7 @@ package oidcupstreamwatcher import ( "bytes" "context" + "crypto/x509" "encoding/base64" "encoding/json" "net/http" @@ -15,11 +16,13 @@ import ( "testing" "time" + coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + expiringcache "k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/util/net" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" @@ -28,9 +31,11 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" supervisorinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -49,7 +54,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { wantDelete bool }{ { - name: "a secret of the right type", + name: "should return true for a secret of the type secrets.pinniped.dev/oidc-client", secret: &corev1.Secret{ Type: "secrets.pinniped.dev/oidc-client", ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, @@ -59,14 +64,34 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { wantDelete: true, }, { - name: "a secret of the wrong type", + name: "should return true for a secret of the type Opaque", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return true for a secret of the type TLS", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "should return false for a secret of the wrong type", secret: &corev1.Secret{ Type: "secrets.pinniped.dev/not-the-oidc-client-type", ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, }, { - name: "resource of wrong data type", + name: "should return false for resource of wrong data type", secret: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, @@ -85,6 +110,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() withInformer := testutil.NewObservableWithInformerOption() var log bytes.Buffer @@ -95,8 +121,10 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { nil, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, + configMapInformer, logger, withInformer.WithInformer, + expiringcache.NewExpiring(), ) unrelated := corev1.Secret{} @@ -109,6 +137,66 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { } } +func TestOIDCUpstreamWatcherControllerFilterConfigMaps(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cm metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any configmap", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fakePinnipedClient := supervisorfake.NewSimpleClientset() + pinnipedInformers := supervisorinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() + cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ + &upstreamoidc.ProviderConfig{Name: "initial-entry"}, + }) + secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() + withInformer := testutil.NewObservableWithInformerOption() + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + New( + cache, + nil, + pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), + secretInformer, + configMapInformer, + logger, + withInformer.WithInformer, + expiringcache.NewExpiring(), + ) + + unrelated := corev1.ConfigMap{} + filter := withInformer.GetFilterForInformer(configMapInformer) + require.Equal(t, test.wantAdd, filter.Add(test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.cm)) + require.Equal(t, test.wantUpdate, filter.Update(test.cm, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(test.cm)) + }) + } +} + func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { t.Parallel() now := metav1.NewTime(time.Now().UTC()) @@ -155,7 +243,8 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { tests := []struct { name string inputUpstreams []runtime.Object - inputSecrets []runtime.Object + inputResources []runtime.Object + inputValidatorCache func(*testing.T) map[oidcDiscoveryCacheKey]*oidcDiscoveryCacheValue wantErr string wantLogs []string wantResultingCache []*oidctestutil.TestUpstreamOIDCIdentityProvider @@ -165,7 +254,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { name: "no upstreams", }, { - name: "missing secret", + name: "missing client Secret", inputUpstreams: []runtime.Object{&idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: idpv1alpha1.OIDCIdentityProviderSpec{ @@ -174,11 +263,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{}, - wantErr: controllerlib.ErrSyntheticRequeue.Error(), + inputResources: []runtime.Object{}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"False","reason":"SecretNotFound","message":"secret \"test-client-secret\" not found"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","reason":"SecretNotFound","message":"secret \"test-client-secret\" not found","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -189,26 +279,18 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "False", - LastTransitionTime: now, - Reason: "SecretNotFound", - Message: `secret "test-client-secret" not found`, - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "discovered issuer configuration", - }, + {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretNotFound", + Message: `secret "test-client-secret" not found`}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration"}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, }, { - name: "secret has wrong type", + name: "client Secret has wrong type", inputUpstreams: []runtime.Object{&idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: idpv1alpha1.OIDCIdentityProviderSpec{ @@ -217,7 +299,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "some-other-type", Data: testValidSecretData, @@ -226,6 +308,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"False","reason":"SecretWrongType","message":"referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","reason":"SecretWrongType","message":"referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -236,20 +319,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "False", - LastTransitionTime: now, - Reason: "SecretWrongType", - Message: `referenced Secret "test-client-secret" has wrong type "some-other-type" (should be "secrets.pinniped.dev/oidc-client")`, - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "discovered issuer configuration", - }, + {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretWrongType", + Message: `referenced Secret "test-client-secret" has wrong type "some-other-type" (should be "secrets.pinniped.dev/oidc-client")`}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration"}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -264,7 +339,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", }}, @@ -272,6 +347,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"False","reason":"SecretMissingKeys","message":"referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","reason":"SecretMissingKeys","message":"referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -282,20 +358,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "False", - LastTransitionTime: now, - Reason: "SecretMissingKeys", - Message: `referenced Secret "test-client-secret" is missing required keys ["clientID" "clientSecret"]`, - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "discovered issuer configuration", - }, + {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretMissingKeys", + Message: `referenced Secret "test-client-secret" is missing required keys ["clientID" "clientSecret"]`}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration"}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -312,7 +380,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -320,9 +388,11 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, - `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidTLSConfig","message":"spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"False","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, - `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidTLSConfig","message":"spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7","error":"OIDCIdentityProvider has a failing condition"}`, + `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7","error":"OIDCIdentityProvider has a failing condition"}`, + `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7","error":"OIDCIdentityProvider has a failing condition"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{}, wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ @@ -331,20 +401,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidTLSConfig", - Message: `spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", + Message: `spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7`}, + {Type: "TLSConfigurationValid", Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7"}, }, }, }}, @@ -361,7 +423,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -369,9 +431,11 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, - `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidTLSConfig","message":"spec.certificateAuthorityData is invalid: no certificates found"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"False","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, - `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidTLSConfig","message":"spec.certificateAuthorityData is invalid: no certificates found","error":"OIDCIdentityProvider has a failing condition"}`, + `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")","error":"OIDCIdentityProvider has a failing condition"}`, + `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","reason":"InvalidTLSConfig","message":"spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")","error":"OIDCIdentityProvider has a failing condition"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{}, wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ @@ -380,20 +444,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidTLSConfig", - Message: `spec.certificateAuthorityData is invalid: no certificates found`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", + Message: `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`}, + {Type: "TLSConfigurationValid", Status: "False", LastTransitionTime: now, Reason: "InvalidTLSConfig", + Message: `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`}, }, }, }}, @@ -407,7 +463,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -416,6 +472,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -426,20 +483,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `failed to parse issuer URL: parse "%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": invalid URL escape "%in"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `failed to parse issuer URL: parse "%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": invalid URL escape "%in"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}, }, }, }}, @@ -453,7 +502,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -462,6 +511,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -472,20 +522,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have "https" scheme, not "http"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have "https" scheme, not "http"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}, }, }, }}, @@ -499,7 +541,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -508,6 +550,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"issuer URL '` + testIssuerURL + `?sub=foo' cannot contain query or fragment component"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"issuer URL '` + testIssuerURL + `?sub=foo' cannot contain query or fragment component","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -518,20 +561,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}, }, }, }}, @@ -545,7 +580,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -554,6 +589,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"issuer URL '` + testIssuerURL + `#fragment' cannot contain query or fragment component"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"issuer URL '` + testIssuerURL + `#fragment' cannot contain query or fragment component","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -564,20 +600,12 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image"}, }, }, }}, @@ -592,7 +620,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -602,6 +630,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateIssuer","message":"failed to perform OIDC discovery","namespace":"test-namespace","name":"test-name","issuer":"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","error":"Get \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -612,21 +641,13 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": -Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee":` + "\n" + + `Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -641,7 +662,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -650,6 +671,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -660,20 +682,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `failed to parse authorization endpoint URL: parse "%": invalid URL escape "%"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `failed to parse authorization endpoint URL: parse "%": invalid URL escape "%"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -688,7 +702,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -697,6 +711,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"failed to parse revocation endpoint URL: parse \"%\": invalid URL escape \"%\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"failed to parse revocation endpoint URL: parse \"%\": invalid URL escape \"%\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -707,20 +722,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `failed to parse revocation endpoint URL: parse "%": invalid URL escape "%"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `failed to parse revocation endpoint URL: parse "%": invalid URL escape "%"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -735,7 +742,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -744,6 +751,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -754,20 +762,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `authorization endpoint URL 'http://example.com/authorize' must have "https" scheme, not "http"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `authorization endpoint URL 'http://example.com/authorize' must have "https" scheme, not "http"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -782,7 +782,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -791,6 +791,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -801,20 +802,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `revocation endpoint URL 'http://example.com/revoke' must have "https" scheme, not "http"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `revocation endpoint URL 'http://example.com/revoke' must have "https" scheme, not "http"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -829,7 +822,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -838,6 +831,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -848,20 +842,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `token endpoint URL 'http://example.com/token' must have "https" scheme, not "http"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `token endpoint URL 'http://example.com/token' must have "https" scheme, not "http"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -876,7 +862,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -885,6 +871,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"token endpoint URL '' must have \"https\" scheme, not \"\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"token endpoint URL '' must have \"https\" scheme, not \"\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -895,20 +882,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `token endpoint URL '' must have "https" scheme, not ""`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `token endpoint URL '' must have "https" scheme, not ""`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -923,7 +902,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -932,6 +911,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"InvalidResponse","message":"authorization endpoint URL '' must have \"https\" scheme, not \"\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"InvalidResponse","message":"authorization endpoint URL '' must have \"https\" scheme, not \"\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -942,20 +922,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "InvalidResponse", - Message: `authorization endpoint URL '' must have "https" scheme, not ""`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "InvalidResponse", + Message: `authorization endpoint URL '' must have "https" scheme, not ""`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -977,12 +949,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []metav1.Condition{ - {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: earlier, Reason: "SomeError1", Message: "some previous error 1"}, - {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: earlier, Reason: "SomeError2", Message: "some previous error 2"}, + {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: earlier, Reason: "SomeError1", + Message: "some previous error 1"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: earlier, Reason: "SomeError2", + Message: "some previous error 2"}, }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -990,6 +964,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -1013,8 +988,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration"}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -1033,12 +1012,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration"}, }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1046,6 +1027,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -1068,9 +1050,240 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []metav1.Condition{ - {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, + }, + }, + }}, + }, + { + name: "valid upstream which already exists in the OIDC discovery validation cache, should skip performing OIDC discovery again and just use cached discovery results", + inputUpstreams: []runtime.Object{&idpv1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: testIssuerURL + "/this-path-does-not-exist", + TLS: &idpv1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, + Claims: idpv1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, + }, + Status: idpv1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + // Was previously validated, so already has conditions. + Conditions: []metav1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, + }, + }, + }}, + inputResources: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }}, + inputValidatorCache: func(t *testing.T) map[oidcDiscoveryCacheKey]*oidcDiscoveryCacheValue { + // Create a working OIDC discovery validator cache entry for the working issuer and CA bundle. + certPool := x509.NewCertPool() + require.True(t, certPool.AppendCertsFromPEM([]byte(testIssuerCA))) + httpClient := phttp.Default(certPool) + httpClient.Timeout = time.Minute // same timeout as in the production code + testCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + // Really do OIDC discovery, so we can put the real result into the cache. + discoveredProvider, err := coreosoidc.NewProvider(coreosoidc.ClientContext(testCtx, httpClient), testIssuerURL) + require.NoError(t, err) + cacheValue := &oidcDiscoveryCacheValue{ + provider: discoveredProvider, + client: httpClient, + } + // Create the cache key to use with the above entry, and cache it at the issuer value that was + // configured in the OIDCIdentityProvider. If the production code tries to perform OIDC discovery + // on that URL, it will fail with a 404. But if the production code correctly reads the pre-cached + // discovery result from this cache, then it should skip discovery and use the value from this cache + // without encountering any errors. + cacheKey := oidcDiscoveryCacheKey{ + issuer: testIssuerURL + "/this-path-does-not-exist", + caBundleHash: tlsconfigutil.NewCABundleHash([]byte(testIssuerCA)), + } + // Put it into the initial cache for this test. + return map[oidcDiscoveryCacheKey]*oidcDiscoveryCacheValue{ + cacheKey: cacheValue, + } + }, + wantLogs: []string{}, + wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ + { + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + RevocationURL: testIssuerRevocationURL, + Scopes: testDefaultExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map + ResourceUID: testUID, + }, + }, + wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: idpv1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + // Conditions are unchanged. + Conditions: []metav1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, + }, + }, + }}, + }, + { + name: "valid upstream with CA bundle read from a Secret", + inputUpstreams: []runtime.Object{&idpv1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: testIssuerURL, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "ca-bundle-secret", + Key: "ca.crt", + }, + }, + Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, + Claims: idpv1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, + }, + }}, + inputResources: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "ca-bundle-secret"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"ca.crt": []byte(testIssuerCA)}, + }, + }, + wantLogs: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, + }, + wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ + { + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + RevocationURL: testIssuerRevocationURL, + Scopes: testDefaultExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map + ResourceUID: testUID, + }, + }, + wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: idpv1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []metav1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, + }, + }, + }}, + }, + { + name: "valid upstream with CA bundle read from a ConfigMap", + inputUpstreams: []runtime.Object{&idpv1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: testIssuerURL, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: "ca-bundle-configmap", + Key: "ca.crt", + }, + }, + Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, + Claims: idpv1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, + }, + }}, + inputResources: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "ca-bundle-configmap"}, + Data: map[string]string{"ca.crt": testIssuerCA}, + }, + }, + wantLogs: []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, + }, + wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ + { + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + RevocationURL: testIssuerRevocationURL, + Scopes: testDefaultExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map + ResourceUID: testUID, + }, + }, + wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: idpv1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []metav1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, }, }, }}, @@ -1089,12 +1302,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration"}, }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1102,6 +1317,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -1124,9 +1340,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []metav1.Condition{ - {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, }, }, }}, @@ -1148,12 +1369,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration"}, }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1161,6 +1384,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -1183,9 +1407,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []metav1.Condition{ - {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, }, }, }}, @@ -1215,12 +1444,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration"}, }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1228,6 +1459,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -1252,9 +1484,14 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []metav1.Condition{ - {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, }, }, }}, @@ -1283,7 +1520,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1292,6 +1529,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"True","reason":"Success","message":"discovered issuer configuration"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"False","reason":"DisallowedParameterName","message":"the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","reason":"DisallowedParameterName","message":"the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -1304,8 +1542,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana {Type: "AdditionalAuthorizeParametersValid", Status: "False", LastTransitionTime: now, Reason: "DisallowedParameterName", Message: "the following additionalAuthorizeParameters are not allowed: " + "response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd", ObservedGeneration: 1234}, - {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle", ObservedGeneration: 1234}, }, }, }}, @@ -1320,7 +1562,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1330,6 +1572,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateIssuer","message":"failed to perform OIDC discovery","namespace":"test-namespace","name":"test-name","issuer":"` + testIssuerURL + `/ends-with-slash","error":"oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\""}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -1340,21 +1583,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/ends-with-slash": -oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/ends-with-slash" got "` + testIssuerURL + `/ends-with-slash/"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/ends-with-slash":` + "\n" + `oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/ends-with-slash" got "` + testIssuerURL + `/ends-with-slash/"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -1369,7 +1603,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs Client: idpv1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, - inputSecrets: []runtime.Object{&corev1.Secret{ + inputResources: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, @@ -1379,6 +1613,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateIssuer","message":"failed to perform OIDC discovery","namespace":"test-namespace","name":"test-name","issuer":"` + testIssuerURL + `/","error":"oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\""}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"ClientCredentialsSecretValid","status":"True","reason":"Success","message":"loaded client credentials"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","status":"False","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\""}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"TLSConfigurationValid","status":"True","reason":"Success","message":"spec.tls is valid: using configured CA bundle"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"test-namespace","name":"test-name","type":"AdditionalAuthorizeParametersValid","status":"True","reason":"Success","message":"additionalAuthorizeParameters parameter names are allowed"}`, `{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"oidc-upstream-observer","caller":"oidcupstreamwatcher/oidc_upstream_watcher.go:$oidcupstreamwatcher.(*oidcWatcherController).validateUpstream","message":"found failing condition","namespace":"test-namespace","name":"test-name","type":"OIDCDiscoverySucceeded","reason":"Unreachable","message":"failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"","error":"OIDCIdentityProvider has a failing condition"}`, }, @@ -1389,21 +1624,13 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs Phase: "Error", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - { - Type: "ClientCredentialsSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded client credentials", - }, - { - Type: "OIDCDiscoverySucceeded", - Status: "False", - LastTransitionTime: now, - Reason: "Unreachable", - Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/": -oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/" got "` + testIssuerURL + `"`, - }, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: now, Reason: "Unreachable", + Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/":` + "\n" + + `oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/" got "` + testIssuerURL + `"`}, + {Type: "TLSConfigurationValid", Status: "True", LastTransitionTime: now, Reason: "Success", + Message: "spec.tls is valid: using configured CA bundle"}, }, }, }}, @@ -1414,7 +1641,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs t.Parallel() fakePinnipedClient := supervisorfake.NewSimpleClientset(tt.inputUpstreams...) pinnipedInformers := supervisorinformers.NewSharedInformerFactory(fakePinnipedClient, 0) - fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) + fakeKubeClient := fake.NewSimpleClientset(tt.inputResources...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ @@ -1424,13 +1651,24 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs var log bytes.Buffer logger := plog.TestLogger(t, &log) + validatorCache := expiringcache.NewExpiring() + if tt.inputValidatorCache != nil { + oidcValidatorCache := &ttlProviderCache{cache: validatorCache} + // add to the underlying validatorCache using oidcValidatorCache which wraps it + for key, value := range tt.inputValidatorCache(t) { + oidcValidatorCache.putProvider(key, value) + } + } + controller := New( cache, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), kubeInformers.Core().V1().Secrets(), + kubeInformers.Core().V1().ConfigMaps(), logger, controllerlib.WithInformer, + validatorCache, ) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 7d3d4f738..ac1ed370e 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -5,8 +5,6 @@ package upstreamwatchers import ( "context" - "crypto/x509" - "encoding/base64" "fmt" "time" @@ -15,40 +13,36 @@ import ( corev1informers "k8s.io/client-go/informers/core/v1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" - "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/tlsconfigutil" "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) const ( - ReasonNotFound = "SecretNotFound" - ReasonWrongType = "SecretWrongType" - ReasonMissingKeys = "SecretMissingKeys" - ReasonSuccess = "Success" - ReasonInvalidTLSConfig = "InvalidTLSConfig" - - ErrNoCertificates = constable.Error("no certificates found") + ReasonNotFound = "SecretNotFound" + ReasonWrongType = "SecretWrongType" + ReasonMissingKeys = "SecretMissingKeys" LDAPBindAccountSecretType = corev1.SecretTypeBasicAuth probeLDAPTimeout = 90 * time.Second // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeLDAPConnectionValid = "LDAPConnectionValid" - TypeSearchBaseFound = "SearchBaseFound" - reasonLDAPConnectionError = "LDAPConnectionError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + typeBindSecretValid = "BindSecretValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + TypeSearchBaseFound = "SearchBaseFound" + reasonLDAPConnectionError = "LDAPConnectionError" + ReasonUsingConfigurationFromSpec = "UsingConfigurationFromSpec" ReasonErrorFetchingSearchBase = "ErrorFetchingSearchBase" ) // ValidatedSettings is the struct which is cached by the ValidatedSettingsCacheI interface. type ValidatedSettings struct { - IDPSpecGeneration int64 // which IDP spec was used during the validation - BindSecretResourceVersion string // which bind secret was used during the validation + IDPSpecGeneration int64 // which IDP spec was used during the validation + BindSecretResourceVersion string // which bind secret was used during the validation + CABundleHash tlsconfigutil.CABundleHash // hash of the CA bundle used during the validation // Cache the setting for TLS vs StartTLS. This is always auto-discovered by probing the server. LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol @@ -135,29 +129,6 @@ type UpstreamGenericLDAPStatus interface { Conditions() []metav1.Condition } -func ValidateTLSConfig(tlsSpec *idpv1alpha1.TLSSpec, config *upstreamldap.ProviderConfig) *metav1.Condition { - if tlsSpec == nil { - return validTLSCondition(noTLSConfigurationMessage) - } - if len(tlsSpec.CertificateAuthorityData) == 0 { - return validTLSCondition(loadedTLSConfigurationMessage) - } - - bundle, err := base64.StdEncoding.DecodeString(tlsSpec.CertificateAuthorityData) - if err != nil { - return invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", err.Error())) - } - - ca := x509.NewCertPool() - ok := ca.AppendCertsFromPEM(bundle) - if !ok { - return invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", ErrNoCertificates)) - } - - config.CABundle = bundle - return validTLSCondition(loadedTLSConfigurationMessage) -} - func TestConnection( ctx context.Context, bindSecretName string, @@ -200,30 +171,12 @@ func TestConnection( return &metav1.Condition{ Type: typeLDAPConnectionValid, Status: metav1.ConditionTrue, - Reason: ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, config.Host, config.BindUsername, bindSecretName, currentSecretVersion), } } -func validTLSCondition(message string) *metav1.Condition { - return &metav1.Condition{ - Type: typeTLSConfigurationValid, - Status: metav1.ConditionTrue, - Reason: ReasonSuccess, - Message: message, - } -} - -func invalidTLSCondition(message string) *metav1.Condition { - return &metav1.Condition{ - Type: typeTLSConfigurationValid, - Status: metav1.ConditionFalse, - Reason: ReasonInvalidTLSConfig, - Message: message, - } -} - func ValidateSecret(secretInformer corev1informers.SecretInformer, secretName string, secretNamespace string, config *upstreamldap.ProviderConfig) (*metav1.Condition, string) { secret, err := secretInformer.Lister().Secrets(secretNamespace).Get(secretName) if err != nil { @@ -260,7 +213,7 @@ func ValidateSecret(secretInformer corev1informers.SecretInformer, secretName st return &metav1.Condition{ Type: typeBindSecretValid, Status: metav1.ConditionTrue, - Reason: ReasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: "loaded bind secret", }, secret.ResourceVersion } @@ -292,6 +245,7 @@ func ValidateGenericLDAP( ctx context.Context, upstream UpstreamGenericLDAPIDP, secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, validatedSettingsCache ValidatedSettingsCacheI, config *upstreamldap.ProviderConfig, ) GradatedConditions { @@ -300,8 +254,10 @@ func ValidateGenericLDAP( secretValidCondition, currentSecretVersion := ValidateSecret(secretInformer, upstream.Spec().BindSecretName(), upstream.Namespace(), config) conditions.Append(secretValidCondition, true) - tlsValidCondition := ValidateTLSConfig(upstream.Spec().TLSSpec(), config) + tlsSpec := tlsconfigutil.TLSSpecForSupervisor(upstream.Spec().TLSSpec()) + tlsValidCondition, caBundle := tlsconfigutil.ValidateTLSConfig(tlsSpec, "spec.tls", upstream.Namespace(), secretInformer, configMapInformer) conditions.Append(tlsValidCondition, true) + config.CABundle = caBundle.PEMBytes() var ldapConnectionValidCondition, searchBaseFoundCondition *metav1.Condition // No point in trying to connect to the server if the config was already determined to be invalid. @@ -325,7 +281,10 @@ func validateAndSetLDAPServerConnectivityAndSearchBase( validatedSettings, hasPreviousValidatedSettings := validatedSettingsCache.Get(upstream.Name(), currentSecretVersion, upstream.Generation()) var ldapConnectionValidCondition, searchBaseFoundCondition *metav1.Condition - if hasPreviousValidatedSettings && validatedSettings.UserSearchBase != "" && validatedSettings.GroupSearchBase != "" { + if hasPreviousValidatedSettings && + validatedSettings.UserSearchBase != "" && + validatedSettings.GroupSearchBase != "" && + validatedSettings.CABundleHash.Equal(tlsconfigutil.NewCABundleHash(config.CABundle)) { // Found previously validated settings in the cache (which is also not missing search base fields), so use them. config.ConnectionProtocol = validatedSettings.LDAPConnectionProtocol config.UserSearch.Base = validatedSettings.UserSearchBase @@ -353,6 +312,7 @@ func validateAndSetLDAPServerConnectivityAndSearchBase( validatedSettingsCache.Set(upstream.Name(), ValidatedSettings{ IDPSpecGeneration: upstream.Generation(), BindSecretResourceVersion: currentSecretVersion, + CABundleHash: tlsconfigutil.NewCABundleHash(config.CABundle), LDAPConnectionProtocol: config.ConnectionProtocol, UserSearchBase: config.UserSearch.Base, GroupSearchBase: config.GroupSearch.Base, diff --git a/internal/controller/tlsconfigutil/ca_bundle.go b/internal/controller/tlsconfigutil/ca_bundle.go new file mode 100644 index 000000000..e4c878cc0 --- /dev/null +++ b/internal/controller/tlsconfigutil/ca_bundle.go @@ -0,0 +1,78 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfigutil + +import ( + "crypto/sha256" + "crypto/x509" +) + +type CABundleHash struct { + hash [32]byte +} + +func NewCABundleHash(bundle []byte) CABundleHash { + return CABundleHash{ + hash: sha256.Sum256(bundle), + } +} + +func (a CABundleHash) Equal(b CABundleHash) bool { + return a == b +} + +// CABundle abstracts the internal representation of CA certificate bundles. +type CABundle struct { + caBundle []byte + sha256 CABundleHash + certPool *x509.CertPool +} + +func NewCABundle(caBundle []byte) (*CABundle, bool) { + var certPool *x509.CertPool + ok := true + + if len(caBundle) > 0 { + certPool = x509.NewCertPool() + ok = certPool.AppendCertsFromPEM(caBundle) + } + + return &CABundle{ + caBundle: caBundle, + sha256: NewCABundleHash(caBundle), + certPool: certPool, + }, ok +} + +// PEMBytes returns the CA certificate bundle PEM bytes. +func (c *CABundle) PEMBytes() []byte { + if c == nil { + return nil + } + return c.caBundle +} + +// PEMString returns the certificate bundle PEM formatted as a string. +func (c *CABundle) PEMString() string { + if c == nil { + return "" + } + return string(c.caBundle) +} + +// CertPool returns a X509 cert pool with the CA certificate bundle. +func (c *CABundle) CertPool() *x509.CertPool { + if c == nil { + return nil + } + return c.certPool +} + +// Hash returns a sha256 sum of the CA bundle bytes. +func (c *CABundle) Hash() CABundleHash { + if c == nil { + return NewCABundleHash(nil) + } + return c.sha256 +} diff --git a/internal/controller/tlsconfigutil/ca_bundle_test.go b/internal/controller/tlsconfigutil/ca_bundle_test.go new file mode 100644 index 000000000..c52d7d932 --- /dev/null +++ b/internal/controller/tlsconfigutil/ca_bundle_test.go @@ -0,0 +1,172 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfigutil + +import ( + "crypto/x509" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/certauthority" +) + +func TestNewCABundleHash(t *testing.T) { + sha256OfNil := CABundleHash{hash: [32]byte{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} + + // On the command line, `echo "test" | shasum -a 256` yields "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + // which is 32 bytes of data encoded as 64 characters. + // https://stackoverflow.com/a/70565837 + // This is the actual binary data: + sha256OfTest := CABundleHash{hash: [32]byte{159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163, 191, 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8}} + + t.Run("will hash the data given", func(t *testing.T) { + caBundleHash := NewCABundleHash([]byte("test")) + + require.True(t, sha256OfTest.Equal(caBundleHash)) + require.Equal(t, sha256OfTest, caBundleHash) + }) + + t.Run("will return the hash of nil input", func(t *testing.T) { + caBundleHash := NewCABundleHash(nil) + + require.True(t, sha256OfNil.Equal(caBundleHash)) + require.Equal(t, sha256OfNil, caBundleHash) + }) + + t.Run("will return the hash of empty input", func(t *testing.T) { + caBundleHash := NewCABundleHash([]byte{}) + + require.True(t, sha256OfNil.Equal(caBundleHash)) + require.Equal(t, sha256OfNil, caBundleHash) + }) +} + +func TestNewCABundle(t *testing.T) { + testCA, err := certauthority.New("Test CA", 1*time.Hour) + require.NoError(t, err) + + t.Run("generates the certPool and hash for certificate input", func(t *testing.T) { + caBundle, ok := NewCABundle(testCA.Bundle()) + require.True(t, ok) + + require.Equal(t, testCA.Bundle(), caBundle.PEMBytes()) + require.Equal(t, NewCABundleHash(testCA.Bundle()), caBundle.Hash()) + require.Equal(t, string(testCA.Bundle()), caBundle.PEMString()) + require.True(t, testCA.Pool().Equal(caBundle.CertPool()), "should be the cert pool of the testCA") + }) + + t.Run("returns false for non-certificate input", func(t *testing.T) { + caBundle, ok := NewCABundle([]byte("here are some bytes")) + require.False(t, ok) + + require.Equal(t, []byte("here are some bytes"), caBundle.PEMBytes()) + require.Equal(t, NewCABundleHash([]byte("here are some bytes")), caBundle.Hash()) + require.Equal(t, "here are some bytes", caBundle.PEMString()) + require.True(t, x509.NewCertPool().Equal(caBundle.CertPool()), "should be an empty cert pool") + }) +} + +func TestPEMBytes(t *testing.T) { + t.Run("returns the CA bundle", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte("here are some bytes")) + + require.Equal(t, []byte("here are some bytes"), caBundle.PEMBytes()) + }) + + t.Run("handles nil bundle by returning nil", func(t *testing.T) { + caBundle, _ := NewCABundle(nil) + require.Nil(t, caBundle.PEMBytes()) + }) + + t.Run("handles empty bundle by returning empty byte array", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte{}) + require.Equal(t, []byte{}, caBundle.PEMBytes()) + }) + + t.Run("handles nil receiver by returning nil", func(t *testing.T) { + var nilCABundle *CABundle + require.Nil(t, nilCABundle.PEMBytes()) + }) +} + +func TestPEMString(t *testing.T) { + t.Run("returns the CA bundle PEM string", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte("here is a string")) + + require.Equal(t, "here is a string", caBundle.PEMString()) + }) + + t.Run("handles nil bundle by returning empty string", func(t *testing.T) { + caBundle, _ := NewCABundle(nil) + + require.Equal(t, "", caBundle.PEMString()) + }) + + t.Run("handles empty bundle by returning empty string", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte{}) + + require.Equal(t, "", caBundle.PEMString()) + }) + + t.Run("handles nil receiver by returning empty string", func(t *testing.T) { + var nilCABundle *CABundle + require.Empty(t, nilCABundle.PEMString()) + }) +} + +func TestCertPool(t *testing.T) { + t.Run("returns the certPool when the caBundle is valid", func(t *testing.T) { + testCA, err := certauthority.New("Test CA", 1*time.Hour) + require.NoError(t, err) + + caBundle, _ := NewCABundle(testCA.Bundle()) + + require.True(t, testCA.Pool().Equal(caBundle.CertPool())) + }) + + t.Run("returns a nil certPool when the caBundle is nil", func(t *testing.T) { + caBundle, _ := NewCABundle(nil) + + require.Nil(t, caBundle.CertPool()) + }) + + t.Run("returns a nil certPool when the caBundle is empty", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte{}) + + require.Nil(t, caBundle.CertPool()) + }) + + t.Run("handles nil receiver by returning nil", func(t *testing.T) { + var nilCABundle *CABundle + require.Nil(t, nilCABundle.CertPool()) + }) +} + +func TestHash(t *testing.T) { + t.Run("returns the Hash of the given CA bundle", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte("this is a CA bundle")) + + require.True(t, NewCABundleHash([]byte("this is a CA bundle")).Equal(caBundle.Hash())) + }) + + t.Run("returns the Hash of nil when the CA bundle is nil", func(t *testing.T) { + caBundle, _ := NewCABundle(nil) + + require.True(t, NewCABundleHash(nil).Equal(caBundle.Hash())) + }) + + t.Run("returns the Hash of nil when the CA bundle is empty", func(t *testing.T) { + caBundle, _ := NewCABundle([]byte{}) + + require.True(t, NewCABundleHash(nil).Equal(caBundle.Hash())) + }) + + t.Run("returns the Hash of nil when the receiver is nil", func(t *testing.T) { + var nilCABundle *CABundle + + require.True(t, NewCABundleHash(nil).Equal(nilCABundle.Hash())) + }) +} diff --git a/internal/controller/tlsconfigutil/tls_config_util.go b/internal/controller/tlsconfigutil/tls_config_util.go new file mode 100644 index 000000000..2c783db6d --- /dev/null +++ b/internal/controller/tlsconfigutil/tls_config_util.go @@ -0,0 +1,257 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfigutil + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1informers "k8s.io/client-go/informers/core/v1" + + authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/controller/conditionsutil" +) + +const ( + ReasonInvalidTLSConfig = "InvalidTLSConfig" + + noTLSConfigurationMessage = "no TLS configuration provided: using default root CA bundle from container image" + loadedTLSConfigurationMessage = "using configured CA bundle" + typeTLSConfigurationValid = "TLSConfigurationValid" +) + +type caBundleSource struct { + Kind string + Name string + Key string +} + +// TLSSpec unifies the TLSSpec type that Supervisor and Concierge both individually define. +// unifying these two definitions to allow sharing code that will read the spec and translate it into a CA bundle. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + CertificateAuthorityData string + // Reference to a CA bundle in a secret or a configmap. + CertificateAuthorityDataSource *caBundleSource +} + +// TLSSpecForSupervisor is a helper function to convert the Supervisor's TLSSpec to the unified TLSSpec. +func TLSSpecForSupervisor(source *idpv1alpha1.TLSSpec) *TLSSpec { + if source == nil { + return nil + } + dest := &TLSSpec{ + CertificateAuthorityData: source.CertificateAuthorityData, + } + + if source.CertificateAuthorityDataSource != nil { + dest.CertificateAuthorityDataSource = &caBundleSource{ + Kind: string(source.CertificateAuthorityDataSource.Kind), + Name: source.CertificateAuthorityDataSource.Name, + Key: source.CertificateAuthorityDataSource.Key, + } + } + + return dest +} + +// TLSSpecForConcierge is a helper function to convert the Concierge's TLSSpec to the unified TLSSpec. +func TLSSpecForConcierge(source *authenticationv1alpha1.TLSSpec) *TLSSpec { + if source == nil { + return nil + } + dest := &TLSSpec{ + CertificateAuthorityData: source.CertificateAuthorityData, + } + if source.CertificateAuthorityDataSource != nil { + dest.CertificateAuthorityDataSource = &caBundleSource{ + Kind: string(source.CertificateAuthorityDataSource.Kind), + Name: source.CertificateAuthorityDataSource.Name, + Key: source.CertificateAuthorityDataSource.Key, + } + } + return dest +} + +// ValidateTLSConfig reads ca bundle in the tlsSpec, supplied either inline using the CertificateAuthorityDate +// or as a reference to a kubernetes secret or configmap using the CertificateAuthorityDataSource, and returns +// - a condition of type TLSConfigurationValid based on the validity of the ca bundle, +// - a CABundle - an abstraction of internal representation of CA certificate bundles. +func ValidateTLSConfig( + tlsSpec *TLSSpec, + conditionPrefix string, + namespace string, + secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, +) (*metav1.Condition, *CABundle) { + caBundle, err := buildCABundle(tlsSpec, conditionPrefix, namespace, secretInformer, configMapInformer) + if err != nil { + return invalidTLSCondition(err.Error()), nil + } + if len(caBundle.PEMBytes()) == 0 { + // An empty or nil CA bundle results in a valid TLS condition which indicates that no CA data was supplied. + return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, noTLSConfigurationMessage)), nil + } + return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, loadedTLSConfigurationMessage)), + caBundle +} + +// buildCABundle reads the unified tlsSpec and returns an X509 cert pool with the CA data that is read either from +// the inline tls.certificateAuthorityData or from a kubernetes secret or a config map as specified in the +// tls.certificateAuthorityDataSource. +// If the provided tlsSpec is nil, a nil CA bundle will be returned. +// If the provided spec contains a CA bundle that is not properly encoded, an error will be returned. +func buildCABundle( + tlsSpec *TLSSpec, + conditionPrefix string, + namespace string, + secretInformer corev1informers.SecretInformer, + configMapInformer corev1informers.ConfigMapInformer, +) (*CABundle, error) { + // if tlsSpec is nil, we return a nil cert pool and cert bundle. A nil error is also returned to indicate that + // a nil tlsSpec is nevertheless a valid one resulting in a valid TLS condition. + if tlsSpec == nil { + return nil, nil + } + + // it is a configuration error to specify a ca bundle inline using the tls.certificateAuthorityDataSource field + // and also specifying a kubernetes secret or a config map to serve as the source for the ca bundle. + if len(tlsSpec.CertificateAuthorityData) > 0 && tlsSpec.CertificateAuthorityDataSource != nil { + return nil, fmt.Errorf("%s is invalid: both tls.certificateAuthorityDataSource and tls.certificateAuthorityData provided", conditionPrefix) + } + + var err error + var caBundleAsBytes []byte + var originalCABundleLength int + + type generateErrorForNoCertsInNonEmptyBundleFunc func() error + var generateErrorForNoCertsInNonEmptyBundle generateErrorForNoCertsInNonEmptyBundleFunc + + if tlsSpec.CertificateAuthorityDataSource != nil { + // CA data read from kubernetes secrets or config maps will not be base64 encoded. + // For kubernetes secrets, secret data read using the client-go code automatically decodes base64 encoded values. + + // track the path of the field in the tlsSpec from which the CA data is sourced. + // this will be used to report in the condition status in case an invalid TLS condition is encountered. + field := fmt.Sprintf("%s.%s", conditionPrefix, "certificateAuthorityDataSource") + var bundleAsString string + bundleAsString, err = readCABundleFromSource(tlsSpec.CertificateAuthorityDataSource, namespace, secretInformer, configMapInformer) + if err != nil { + return nil, fmt.Errorf("%s is invalid: %s", field, err.Error()) + } + caBundleAsBytes = []byte(bundleAsString) + originalCABundleLength = len(bundleAsString) + + generateErrorForNoCertsInNonEmptyBundle = func() error { + namespacedName := fmt.Sprintf("%s/%s", namespace, tlsSpec.CertificateAuthorityDataSource.Name) + + return fmt.Errorf(`%s is invalid: key %q with %d bytes of data in %s %q is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + field, tlsSpec.CertificateAuthorityDataSource.Key, originalCABundleLength, strings.ToLower(tlsSpec.CertificateAuthorityDataSource.Kind), namespacedName) + } + } else { + // the ca data supplied inline in the CRDs is expected to be base64 encoded. + field := fmt.Sprintf("%s.%s", conditionPrefix, "certificateAuthorityData") + var decodedBytes []byte + decodedBytes, err = base64.StdEncoding.DecodeString(tlsSpec.CertificateAuthorityData) + if err != nil { + return nil, fmt.Errorf("%s is invalid: %s", field, err.Error()) + } + + caBundleAsBytes = decodedBytes + originalCABundleLength = len(tlsSpec.CertificateAuthorityData) + + generateErrorForNoCertsInNonEmptyBundle = func() error { + return fmt.Errorf("%s is invalid: no base64-encoded PEM certificates found in %d bytes of data (PEM certificates must begin with \"-----BEGIN CERTIFICATE-----\")", + field, originalCABundleLength) + } + } + + // It is perfectly valid to have an empty CA bundle + if originalCABundleLength == 0 { + return nil, nil + } + + caBundle, ok := NewCABundle(caBundleAsBytes) + if !ok { + return nil, generateErrorForNoCertsInNonEmptyBundle() + } + + return caBundle, nil +} + +func readCABundleFromSource(source *caBundleSource, namespace string, secretInformer corev1informers.SecretInformer, configMapInformer corev1informers.ConfigMapInformer) (string, error) { + switch source.Kind { + case "Secret": + return readCABundleFromK8sSecret(namespace, source.Name, source.Key, secretInformer) + case "ConfigMap": + return readCABundleFromK8sConfigMap(namespace, source.Name, source.Key, configMapInformer) + default: + return "", fmt.Errorf("unsupported CA bundle source kind: %s", source.Kind) + } +} + +func readCABundleFromK8sSecret(namespace string, name string, key string, secretInformer corev1informers.SecretInformer) (string, error) { + namespacedName := fmt.Sprintf("%s/%s", namespace, name) + + s, err := secretInformer.Lister().Secrets(namespace).Get(name) + if err != nil { + return "", errors.Wrapf(err, "failed to get secret %q", namespacedName) + } + + // For Secrets to be used as a certificate authority data source, the secret should be of type + // kubernetes.io/tls or Opaque. It is an error to use a secret that is of any other type. + if s.Type != corev1.SecretTypeTLS && s.Type != corev1.SecretTypeOpaque { + return "", fmt.Errorf("secret %q of type %q cannot be used as a certificate authority data source", namespacedName, s.Type) + } + + val, exists := s.Data[key] + if !exists { + return "", fmt.Errorf("key %q not found in secret %q", key, namespacedName) + } + if len(val) == 0 { + return "", fmt.Errorf("key %q has empty value in secret %q", key, namespacedName) + } + return string(val), nil +} + +func readCABundleFromK8sConfigMap(namespace string, name string, key string, configMapInformer corev1informers.ConfigMapInformer) (string, error) { + namespacedName := fmt.Sprintf("%s/%s", namespace, name) + + c, err := configMapInformer.Lister().ConfigMaps(namespace).Get(name) + if err != nil { + return "", errors.Wrapf(err, "failed to get configmap %q", namespacedName) + } + + val, exists := c.Data[key] + if !exists { + return "", fmt.Errorf("key %q not found in configmap %q", key, namespacedName) + } + if len(val) == 0 { + return "", fmt.Errorf("key %q has empty value in configmap %q", key, namespacedName) + } + return val, nil +} + +func validTLSCondition(message string) *metav1.Condition { + return &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: message, + } +} + +func invalidTLSCondition(message string) *metav1.Condition { + return &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: message, + } +} diff --git a/internal/controller/tlsconfigutil/tls_config_util_test.go b/internal/controller/tlsconfigutil/tls_config_util_test.go new file mode 100644 index 000000000..230432e46 --- /dev/null +++ b/internal/controller/tlsconfigutil/tls_config_util_test.go @@ -0,0 +1,654 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfigutil + +import ( + "context" + "encoding/base64" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes/fake" + + authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/controller/conditionsutil" +) + +func TestValidateTLSConfig(t *testing.T) { + testCA, err := certauthority.New("Test CA", 1*time.Hour) + require.NoError(t, err) + base64EncodedBundle := base64.StdEncoding.EncodeToString(testCA.Bundle()) + + testCABundle, ok := NewCABundle(testCA.Bundle()) + require.True(t, ok) + + tests := []struct { + name string + tlsSpec *TLSSpec + namespace string + k8sObjects []runtime.Object + expectedCABundle *CABundle + expectedCondition *metav1.Condition + }{ + { + name: "nil TLSSpec should generate a noTLSConfigurationMessage condition", + tlsSpec: nil, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: " + noTLSConfigurationMessage, + }, + }, + { + name: "empty inline ca data should generate a loadedTLSConfigurationMessage condition", + tlsSpec: &TLSSpec{}, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: " + noTLSConfigurationMessage, + }, + }, + { + name: "valid base64 encode ca data should generate a loadedTLSConfigurationMessage condition", + tlsSpec: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + }, + expectedCABundle: testCABundle, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: " + loadedTLSConfigurationMessage, + }, + }, + { + name: "valid base64 encoded non cert data should generate a invalidTLSCondition condition", + tlsSpec: &TLSSpec{ + CertificateAuthorityData: "dGhpcyBpcyBzb21lIHRlc3QgZGF0YSB0aGF0IGlzIGJhc2U2NCBlbmNvZGVkIHRoYXQgaXMgbm90IGEgY2VydAo=", + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 88 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + }, + }, + { + name: "non-base64 encoded string as ca data should generate an invalidTLSCondition condition", + tlsSpec: &TLSSpec{ + CertificateAuthorityData: "non base64 encoded string", + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: "spec.foo.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 3", + }, + }, + { + name: "supplying certificateAuthorityDataSource and certificateAuthorityData should generate an invalid condition", + tlsSpec: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "super-secret", + Key: "ca-base64EncodedBundle", + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: "spec.foo.tls is invalid: both tls.certificateAuthorityDataSource and tls.certificateAuthorityData provided", + }, + }, + { + name: "should return ca bundle from kubernetes secret of type tls", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret-tls", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret-tls", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca-bundle": testCA.Bundle(), + }, + }, + }, + expectedCABundle: testCABundle, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: using configured CA bundle", + }, + }, + { + name: "should return ca bundle from kubernetes secret of type opaque", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret-opaque", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret-opaque", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca-bundle": testCA.Bundle(), + }, + }, + }, + expectedCABundle: testCABundle, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: using configured CA bundle", + }, + }, + { + name: "should return invalid condition when a secrets not of type tls or opaque are used as ca data source", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret-ba", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret-ba", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeBasicAuth, + Data: map[string][]byte{ + "ca-bundle": testCA.Bundle(), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: secret "awesome-namespace/awesome-secret-ba" of type "kubernetes.io/basic-auth" cannot be used as a certificate authority data source`, + }, + }, + { + name: "should return invalid condition when a secret does not have the configured key", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "wrong-key": testCA.Bundle(), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" not found in secret "awesome-namespace/awesome-secret"`, + }, + }, + { + name: "should return invalid condition when a secret has the configured key but its value is empty", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca-bundle": []byte(""), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" has empty value in secret "awesome-namespace/awesome-secret"`, + }, + }, + { + name: "should return invalid condition when a secret has the configured key but the value is not a cert", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-secret", + Namespace: "awesome-namespace", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca-bundle": []byte("this is not a certificate"), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" with 25 bytes of data in secret "awesome-namespace/awesome-secret" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + }, + }, + { + name: "should return invalid condition when a configmap does not have the configured key", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "ConfigMap", + Name: "awesome-configmap", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-configmap", + Namespace: "awesome-namespace", + }, + Data: map[string]string{ + "wrong-key": string(testCA.Bundle()), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" not found in configmap "awesome-namespace/awesome-configmap"`, + }, + }, + { + name: "should return invalid condition when a configmap has the configured key but its value is empty", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "ConfigMap", + Name: "awesome-configmap", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-configmap", + Namespace: "awesome-namespace", + }, + Data: map[string]string{ + "ca-bundle": "", + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" has empty value in configmap "awesome-namespace/awesome-configmap"`, + }, + }, + { + name: "should return invalid condition when a configmap has the configured key but its value not a cert", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "ConfigMap", + Name: "awesome-configmap", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-configmap", + Namespace: "awesome-namespace", + }, + Data: map[string]string{ + "ca-bundle": "this is not a cert", + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: key "ca-bundle" with 18 bytes of data in configmap "awesome-namespace/awesome-configmap" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + }, + }, + { + name: "should return ca bundle from kubernetes configMap", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "ConfigMap", + Name: "awesome-cm", + Key: "ca-bundle", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-cm", + Namespace: "awesome-namespace", + }, + Data: map[string]string{ + "ca-bundle": string(testCA.Bundle()), + }, + }, + }, + expectedCABundle: testCABundle, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: conditionsutil.ReasonSuccess, + Message: "spec.foo.tls is valid: using configured CA bundle", + }, + }, + { + name: "should return invalid condition when failing to read ca bundle from kubernetes secret that does not exist", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "does-not-exist", + Key: "does-not-matter", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{}, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: failed to get secret "awesome-namespace/does-not-exist": secret "does-not-exist" not found`, + }, + }, + { + name: "should return invalid condition when failing to read ca bundle from kubernetes configMap that does not exist", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "ConfigMap", + Name: "does-not-exist", + Key: "does-not-matter", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{}, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: `spec.foo.tls.certificateAuthorityDataSource is invalid: failed to get configmap "awesome-namespace/does-not-exist": configmap "does-not-exist" not found`, + }, + }, + { + name: "should return invalid condition when using an invalid certificate authority data source", + tlsSpec: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "SomethingElse", + Name: "does-not-exist", + Key: "does-not-matter", + }, + }, + namespace: "awesome-namespace", + k8sObjects: []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "awesome-cm", + Namespace: "awesome-namespace", + }, + Data: map[string]string{ + "ca-bundle": string(testCA.Bundle()), + }, + }, + }, + expectedCondition: &metav1.Condition{ + Type: typeTLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: "spec.foo.tls.certificateAuthorityDataSource is invalid: unsupported CA bundle source kind: SomethingElse", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var secretsInformer corev1informers.SecretInformer + var configMapInformer corev1informers.ConfigMapInformer + + fakeClient := fake.NewSimpleClientset(tt.k8sObjects...) + sharedInformers := informers.NewSharedInformerFactory(fakeClient, 0) + configMapInformer = sharedInformers.Core().V1().ConfigMaps() + secretsInformer = sharedInformers.Core().V1().Secrets() + + // Calling the Informer() function registers this informer in the sharedinformer. + // Doing this will ensure that this informer will be sync'd when Start() is called. + // This is needed in this test because we are not using the controller library here, + // which would do these same calls for us. + configMapInformer.Informer() + secretsInformer.Informer() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + sharedInformers.Start(ctx.Done()) + // This is needed in this test because we are not using the controller library here, + // which would do this same call for us. + sharedInformers.WaitForCacheSync(ctx.Done()) + + actualCondition, actualBundle := ValidateTLSConfig(tt.tlsSpec, "spec.foo.tls", tt.namespace, secretsInformer, configMapInformer) + + require.Equal(t, tt.expectedCondition, actualCondition) + if tt.expectedCABundle != nil { + require.Equal(t, tt.expectedCABundle.Hash(), actualBundle.Hash()) + require.Equal(t, tt.expectedCABundle.PEMBytes(), actualBundle.PEMBytes()) + require.True(t, tt.expectedCABundle.CertPool().Equal(actualBundle.CertPool())) + } + }) + } +} + +func TestTLSSpecForSupervisor(t *testing.T) { + testCA, err := certauthority.New("Test CA", 1*time.Hour) + require.NoError(t, err) + bundle := testCA.Bundle() + base64EncodedBundle := base64.StdEncoding.EncodeToString(bundle) + + tests := []struct { + name string + supervisorTLSSpec *idpv1alpha1.TLSSpec + expected *TLSSpec + }{ + { + name: "should return nil spec when supervisorTLSSpec is nil", + supervisorTLSSpec: nil, + expected: nil, + }, + { + name: "should return tls spec with non-empty certificateAuthorityData", + supervisorTLSSpec: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: nil, + }, + expected: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: nil, + }, + }, + { + name: "should return tls spec with certificateAuthorityDataSource", + supervisorTLSSpec: &idpv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + expected: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + }, + { + name: "should return tls spec when source has all fields filled", + supervisorTLSSpec: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + expected: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actual := TLSSpecForSupervisor(tt.supervisorTLSSpec) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestTLSSpecForConcierge(t *testing.T) { + testCA, err := certauthority.New("Test CA", 1*time.Hour) + require.NoError(t, err) + bundle := testCA.Bundle() + base64EncodedBundle := base64.StdEncoding.EncodeToString(bundle) + + tests := []struct { + name string + conciergeTLSSpec *authenticationv1alpha1.TLSSpec + expected *TLSSpec + }{ + { + name: "should return nil spec when TLSSpec is nil", + conciergeTLSSpec: nil, + expected: nil, + }, + { + name: "should return tls spec with non-empty certificateAuthorityData", + conciergeTLSSpec: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: nil, + }, + expected: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: nil, + }, + }, + { + name: "should return tls spec with certificateAuthorityDataSource", + conciergeTLSSpec: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + expected: &TLSSpec{ + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + }, + { + name: "should return tls spec when source has all fields filled", + conciergeTLSSpec: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + expected: &TLSSpec{ + CertificateAuthorityData: base64EncodedBundle, + CertificateAuthorityDataSource: &caBundleSource{ + Kind: "Secret", + Name: "awesome-secret", + Key: "ca-bundle", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actual := TLSSpecForConcierge(tt.conciergeTLSSpec) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/controller/utils.go b/internal/controller/utils.go index e280cce68..e41b34dab 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -4,17 +4,11 @@ package controller import ( - "crypto/x509" - "encoding/base64" - "fmt" "slices" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/cert" - authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" - idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/controllerlib" ) @@ -52,6 +46,10 @@ func SimpleFilter(match func(metav1.Object) bool, parentFunc controllerlib.Paren } func MatchAnySecretOfTypeFilter(secretType corev1.SecretType, parentFunc controllerlib.ParentFunc, namespaces ...string) controllerlib.Filter { + return MatchAnySecretOfTypesFilter([]corev1.SecretType{secretType}, parentFunc, namespaces...) +} + +func MatchAnySecretOfTypesFilter(secretTypes []corev1.SecretType, parentFunc controllerlib.ParentFunc, namespaces ...string) controllerlib.Filter { isSecretOfType := func(obj metav1.Object) bool { secret, ok := obj.(*corev1.Secret) if !ok { @@ -61,7 +59,7 @@ func MatchAnySecretOfTypeFilter(secretType corev1.SecretType, parentFunc control if len(namespaces) > 0 && !slices.Contains(namespaces, secret.Namespace) { return false } - return secret.Type == secretType + return slices.Contains(secretTypes, secret.Type) } return SimpleFilter(isSecretOfType, parentFunc) } @@ -99,43 +97,3 @@ type WithInformerOptionFunc func( // Same signature as controllerlib.WithInitialEvent(). type WithInitialEventOptionFunc func(key controllerlib.Key) controllerlib.Option - -// BuildCertPoolAuth returns a PEM-encoded CA bundle from the provided spec. If the provided spec is nil, a -// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly -// encoded, an error will be returned. -func BuildCertPoolAuth(spec *authenticationv1alpha1.TLSSpec) (*x509.CertPool, []byte, error) { - if spec == nil { - return nil, nil, nil - } - - return buildCertPool(spec.CertificateAuthorityData) -} - -// BuildCertPoolIDP returns a PEM-encoded CA bundle from the provided spec. If the provided spec is nil, a -// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly -// encoded, an error will be returned. -func BuildCertPoolIDP(spec *idpv1alpha1.TLSSpec) (*x509.CertPool, []byte, error) { - if spec == nil { - return nil, nil, nil - } - - return buildCertPool(spec.CertificateAuthorityData) -} - -func buildCertPool(certificateAuthorityData string) (*x509.CertPool, []byte, error) { - if len(certificateAuthorityData) == 0 { - return nil, nil, nil - } - - pem, err := base64.StdEncoding.DecodeString(certificateAuthorityData) - if err != nil { - return nil, nil, err - } - - rootCAs, err := cert.NewPoolFromBytes(pem) - if err != nil { - return nil, nil, fmt.Errorf("certificateAuthorityData is not valid PEM: %w", err) - } - - return rootCAs, pem, nil -} diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index a7edc4fd4..6d5144216 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -235,9 +235,13 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol // authenticators up to date. WithController( webhookcachefiller.New( + c.ServerInstallationInfo.Namespace, c.AuthenticatorCache, client.PinnipedConcierge, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), + informers.installationNamespaceK8s.Core().V1().Secrets(), + informers.installationNamespaceK8s.Core().V1().ConfigMaps(), + controllerlib.WithInformer, clock.RealClock{}, plog.New(), ), @@ -245,9 +249,13 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol ). WithController( jwtcachefiller.New( + c.ServerInstallationInfo.Namespace, c.AuthenticatorCache, client.PinnipedConcierge, informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), + informers.installationNamespaceK8s.Core().V1().Secrets(), + informers.installationNamespaceK8s.Core().V1().ConfigMaps(), + controllerlib.WithInformer, clock.RealClock{}, plog.New(), ), diff --git a/internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go b/internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go index 3058f24ea..5917c2304 100644 --- a/internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go +++ b/internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go @@ -13,6 +13,7 @@ import ( supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/oidcclientsecretstorage" ) @@ -23,7 +24,6 @@ const ( allowedGrantTypesValid = "AllowedGrantTypesValid" allowedScopesValid = "AllowedScopesValid" - reasonSuccess = "Success" reasonMissingRequiredValue = "MissingRequiredValue" reasonNoClientSecretFound = "NoClientSecretFound" reasonInvalidClientSecretFound = "InvalidClientSecretFound" @@ -79,7 +79,7 @@ func validateAllowedScopes(oidcClient *supervisorconfigv1alpha1.OIDCClient, cond conditions = append(conditions, &metav1.Condition{ Type: allowedScopesValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("%q is valid", allowedScopesFieldName), }) } else { @@ -115,7 +115,7 @@ func validateAllowedGrantTypes(oidcClient *supervisorconfigv1alpha1.OIDCClient, conditions = append(conditions, &metav1.Condition{ Type: allowedGrantTypesValid, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("%q is valid", allowedGrantTypesFieldName), }) } else { @@ -201,7 +201,7 @@ func validateSecret(secret *corev1.Secret, conditions []*metav1.Condition, minBc conditions = append(conditions, &metav1.Condition{ Type: clientSecretExists, Status: metav1.ConditionTrue, - Reason: reasonSuccess, + Reason: conditionsutil.ReasonSuccess, Message: fmt.Sprintf("%d client secret(s) found", storedClientSecretsCount), }) return conditions, storedClientSecrets diff --git a/internal/mocks/mockcachevalue/generate.go b/internal/mocks/mockcachevalue/generate.go new file mode 100644 index 000000000..47447114a --- /dev/null +++ b/internal/mocks/mockcachevalue/generate.go @@ -0,0 +1,6 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package mockcachevalue + +//go:generate go run -v go.uber.org/mock/mockgen -destination=mockcachevalue.go -package=mockcachevalue -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/controller/authenticator/authncache Value diff --git a/internal/mocks/mockcachevalue/mockcachevalue.go b/internal/mocks/mockcachevalue/mockcachevalue.go new file mode 100644 index 000000000..6339b35a4 --- /dev/null +++ b/internal/mocks/mockcachevalue/mockcachevalue.go @@ -0,0 +1,73 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: go.pinniped.dev/internal/controller/authenticator/authncache (interfaces: Value) +// +// Generated by this command: +// +// mockgen -destination=mockcachevalue.go -package=mockcachevalue -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/controller/authenticator/authncache Value +// + +// Package mockcachevalue is a generated GoMock package. +package mockcachevalue + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + authenticator "k8s.io/apiserver/pkg/authentication/authenticator" +) + +// MockValue is a mock of Value interface. +type MockValue struct { + ctrl *gomock.Controller + recorder *MockValueMockRecorder +} + +// MockValueMockRecorder is the mock recorder for MockValue. +type MockValueMockRecorder struct { + mock *MockValue +} + +// NewMockValue creates a new mock instance. +func NewMockValue(ctrl *gomock.Controller) *MockValue { + mock := &MockValue{ctrl: ctrl} + mock.recorder = &MockValueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockValue) EXPECT() *MockValueMockRecorder { + return m.recorder +} + +// AuthenticateToken mocks base method. +func (m *MockValue) AuthenticateToken(arg0 context.Context, arg1 string) (*authenticator.Response, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthenticateToken", arg0, arg1) + ret0, _ := ret[0].(*authenticator.Response) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// AuthenticateToken indicates an expected call of AuthenticateToken. +func (mr *MockValueMockRecorder) AuthenticateToken(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateToken", reflect.TypeOf((*MockValue)(nil).AuthenticateToken), arg0, arg1) +} + +// Close mocks base method. +func (m *MockValue) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockValueMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockValue)(nil).Close)) +} diff --git a/internal/mocks/mocktokenauthenticator/generate.go b/internal/mocks/mocktokenauthenticator/generate.go deleted file mode 100644 index 13329bfcc..000000000 --- a/internal/mocks/mocktokenauthenticator/generate.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package mocktokenauthenticator - -//go:generate go run -v go.uber.org/mock/mockgen -destination=mocktokenauthenticator.go -package=mocktokenauthenticator -copyright_file=../../../hack/header.txt k8s.io/apiserver/pkg/authentication/authenticator Token diff --git a/internal/mocks/mocktokenauthenticator/mocktokenauthenticator.go b/internal/mocks/mocktokenauthenticator/mocktokenauthenticator.go deleted file mode 100644 index 86a158084..000000000 --- a/internal/mocks/mocktokenauthenticator/mocktokenauthenticator.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: k8s.io/apiserver/pkg/authentication/authenticator (interfaces: Token) -// -// Generated by this command: -// -// mockgen -destination=mocktokenauthenticator.go -package=mocktokenauthenticator -copyright_file=../../../hack/header.txt k8s.io/apiserver/pkg/authentication/authenticator Token -// - -// Package mocktokenauthenticator is a generated GoMock package. -package mocktokenauthenticator - -import ( - context "context" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" - authenticator "k8s.io/apiserver/pkg/authentication/authenticator" -) - -// MockToken is a mock of Token interface. -type MockToken struct { - ctrl *gomock.Controller - recorder *MockTokenMockRecorder -} - -// MockTokenMockRecorder is the mock recorder for MockToken. -type MockTokenMockRecorder struct { - mock *MockToken -} - -// NewMockToken creates a new mock instance. -func NewMockToken(ctrl *gomock.Controller) *MockToken { - mock := &MockToken{ctrl: ctrl} - mock.recorder = &MockTokenMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockToken) EXPECT() *MockTokenMockRecorder { - return m.recorder -} - -// AuthenticateToken mocks base method. -func (m *MockToken) AuthenticateToken(arg0 context.Context, arg1 string) (*authenticator.Response, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthenticateToken", arg0, arg1) - ret0, _ := ret[0].(*authenticator.Response) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// AuthenticateToken indicates an expected call of AuthenticateToken. -func (mr *MockTokenMockRecorder) AuthenticateToken(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateToken", reflect.TypeOf((*MockToken)(nil).AuthenticateToken), arg0, arg1) -} diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index e3d796127..c2fdceb4b 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/cache" apimachineryversion "k8s.io/apimachinery/pkg/version" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" @@ -154,6 +155,7 @@ func prepareControllers( federationDomainInformer := pinnipedInformers.Config().V1alpha1().FederationDomains() oidcClientInformer := pinnipedInformers.Config().V1alpha1().OIDCClients() secretInformer := kubeInformers.Core().V1().Secrets() + configMapInformer := kubeInformers.Core().V1().ConfigMaps() // Create controller manager. controllerManager := controllerlib. @@ -303,8 +305,10 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, + configMapInformer, plog.New(), controllerlib.WithInformer, + cache.NewExpiring(), ), singletonWorker). WithController( @@ -313,6 +317,7 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), secretInformer, + configMapInformer, controllerlib.WithInformer, ), singletonWorker). @@ -322,6 +327,7 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), secretInformer, + configMapInformer, controllerlib.WithInformer, ), singletonWorker). @@ -332,10 +338,12 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().GitHubIdentityProviders(), secretInformer, + configMapInformer, plog.New(), controllerlib.WithInformer, clock.RealClock{}, tls.Dial, + cache.NewExpiring(), ), singletonWorker). WithController( diff --git a/internal/testutil/conciergetestutil/tlstestutil.go b/internal/testutil/conciergetestutil/tlstestutil.go deleted file mode 100644 index 1f275c89f..000000000 --- a/internal/testutil/conciergetestutil/tlstestutil.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package conciergetestutil - -import ( - "crypto/tls" - "encoding/base64" - "encoding/pem" - - authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" -) - -func TLSSpecFromTLSConfig(tls *tls.Config) *authenticationv1alpha1.TLSSpec { - pemData := make([]byte, 0) - for _, certificate := range tls.Certificates { - // this is the public part of the certificate, the private is the certificate.PrivateKey - for _, reallyCertificate := range certificate.Certificate { - pemData = append(pemData, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: reallyCertificate, - })...) - } - } - return &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemData), - } -} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 2821a4483..9beee329c 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -74,7 +74,7 @@ const ( TLS = LDAPConnectionProtocol("TLS") ) -// ProviderConfig includes all of the settings for connection and searching for users and groups in +// ProviderConfig includes all the settings for connection and searching for users and groups in // the upstream LDAP IDP. It also provides methods for testing the connection and performing logins. // The nested structs are not pointer fields to enable deep copy on function params and return values. type ProviderConfig struct { @@ -372,7 +372,7 @@ func (p *Provider) tlsConfig() (*tls.Config, error) { return ptls.DefaultLDAP(rootCAs), nil } -// GetName returns a name for this upstream provider. +// GetResourceName returns a name for this upstream provider. func (p *Provider) GetResourceName() string { return p.c.Name } diff --git a/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md b/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md index d9089775b..05905096b 100644 --- a/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md +++ b/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md @@ -56,6 +56,9 @@ spec: # If the TLS certificate of your FederationDomain is not signed by # a standard CA trusted by the Concierge pods by default, then # specify its CA here as a base64-encoded PEM. + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). tls: certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0...0tLQo= ``` diff --git a/site/content/docs/howto/concierge/configure-concierge-webhook.md b/site/content/docs/howto/concierge/configure-concierge-webhook.md index 0b0b0fe4f..c38325371 100644 --- a/site/content/docs/howto/concierge/configure-concierge-webhook.md +++ b/site/content/docs/howto/concierge/configure-concierge-webhook.md @@ -37,7 +37,10 @@ spec: # HTTPS endpoint to be called as a webhook endpoint: https://my-webhook.example.com/any/path tls: - # base64-encoded PEM CA bundle (optional) + # Base64-encoded PEM CA bundle for connections to webhook (optional). + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). certificateAuthorityData: "LS0tLS1CRUdJTi[...]" ``` diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md index 55c8fc416..ab088a411 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md @@ -97,6 +97,12 @@ spec: # Specify the host of the Active Directory server. host: "activedirectory.example.com:636" + tls: + # Base64-encoded PEM CA bundle for connections to AD (optional). + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). + certificateAuthorityData: "LS0tLS1CRUdJTi[...]" # Specify how to search for the username when an end-user tries to log in # using their username and password. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md index db5a8879c..81ff4f9aa 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md @@ -86,6 +86,12 @@ metadata: spec: # Specify the upstream issuer URL (no trailing slash). issuer: https:// + tls: + # Base64-encoded PEM CA bundle for connections to Dex (optional). + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). + certificateAuthorityData: "LS0tLS1CRUdJTi[...]" # Specify how to form authorization requests to Dex. authorizationConfig: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-github.md b/site/content/docs/howto/supervisor/configure-supervisor-with-github.md index 60d85bed6..eb9bbcc83 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-github.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-github.md @@ -221,6 +221,9 @@ spec: # This field is usually only used for GitHub Enterprise Server. # Specify the CA certificate of the server as a # base64-encoded PEM bundle. + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FU.... client: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md index d0871160b..18b56627b 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md @@ -158,6 +158,9 @@ spec: # Specify the CA bundle for the GitLab server as base64-encoded PEM # data. For example, the output of `cat my-ca-bundle.pem | base64`. + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). # # This is only necessary if your instance uses a custom CA. tls: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md index 46aff6716..996c11a31 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md @@ -210,6 +210,9 @@ spec: # Specify the CA certificate of the LDAP server as a # base64-encoded PEM bundle. + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). tls: certificateAuthorityData: $(cat ca.pem | base64) diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md index a1e18a441..17b611ddb 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md @@ -70,6 +70,12 @@ spec: # actual issuer of your Workspace ONE Access environment. Note that # the Workspace ONE Access issuer ends with the string "/SAAS/auth". issuer: https://ws1.my-company.com/SAAS/auth + tls: + # Base64-encoded PEM CA bundle for connections to WS1 (optional). + # Alternatively, the CA bundle can be specified in a Secret or + # ConfigMap that will be dynamically watched by Pinniped for + # changes to the CA bundle (see API docs for details). + certificateAuthorityData: "LS0tLS1CRUdJTi[...]" # Specify how to form authorization requests to Workspace ONE Access. authorizationConfig: @@ -138,7 +144,7 @@ remaining claims are always available. "Test Group" ], "iss": "https://ws1.my-company.com/SAAS/auth", - "sub": "my-username@WS1-ENV-NAME", + "sub": "my-username@WS1-ENV-NAME" } ``` diff --git a/test/integration/concierge_client_test.go b/test/integration/concierge_client_test.go index 7ea58ee97..78b3c8ca1 100644 --- a/test/integration/concierge_client_test.go +++ b/test/integration/concierge_client_test.go @@ -5,11 +5,13 @@ package integration import ( "context" + "encoding/base64" "strings" "testing" "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" "go.pinniped.dev/internal/here" @@ -59,34 +61,103 @@ func TestClient(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) - - // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. - invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey) - _, err := invalidClient.Discovery().ServerVersion() - require.EqualError(t, err, "the server has asked for the client to provide credentials") - - // Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange. - clientConfig := testlib.NewClientConfig(t) - client, err := conciergeclient.New( - conciergeclient.WithCABundle(string(clientConfig.CAData)), - conciergeclient.WithEndpoint(clientConfig.Host), - conciergeclient.WithAuthenticator("webhook", webhook.Name), - conciergeclient.WithAPIGroupSuffix(env.APIGroupSuffix), - ) + defaultWebhook := &testlib.IntegrationEnv(t).TestWebhook + TLSCABundle, err := base64.StdEncoding.DecodeString(env.TestWebhook.TLS.CertificateAuthorityData) require.NoError(t, err) - testlib.RequireEventually(t, func(requireEventually *require.Assertions) { - resp, err := client.ExchangeToken(ctx, env.TestUser.Token) - requireEventually.NoError(err) - requireEventually.NotNil(resp.Status.ExpirationTimestamp) - requireEventually.InDelta(5*time.Minute, time.Until(resp.Status.ExpirationTimestamp.Time), float64(time.Minute)) + tests := []struct { + name string + edit func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) + }{ + { + name: "default webhook authenticator", + edit: nil, + }, + { + name: "webhook authenticator with secret of type TLS to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": string(TLSCABundle), + "tls.crt": "", + "tls.key": "", + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }, + }, + { + name: "webhook authenticator with secret of type opaque to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": string(TLSCABundle), + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }, + }, + { + name: "webhook authenticator with configmap to source ca bundle", + edit: func(t *testing.T, spec *authenticationv1alpha1.WebhookAuthenticatorSpec) { + caConfigmap := testlib.CreateTestConfigMap(t, env.ConciergeNamespace, "ca-cert", + map[string]string{ + "ca.crt": string(TLSCABundle), + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caConfigmap.Name, + Key: "ca.crt", + } + }, + }, + } - // Create a client using the certificate and key returned by the token exchange. - validClient := testlib.NewClientsetWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + webhookSpec := defaultWebhook.DeepCopy() + if test.edit != nil { + test.edit(t, webhookSpec) + } + webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, webhookSpec, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) - // Make a version request, which should succeed even without any authorization. - _, err = validClient.Discovery().ServerVersion() - requireEventually.NoError(err) - }, 10*time.Second, 500*time.Millisecond) + // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. + invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey) + _, err := invalidClient.Discovery().ServerVersion() + require.EqualError(t, err, "the server has asked for the client to provide credentials") + + // Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange. + clientConfig := testlib.NewClientConfig(t) + client, err := conciergeclient.New( + conciergeclient.WithCABundle(string(clientConfig.CAData)), + conciergeclient.WithEndpoint(clientConfig.Host), + conciergeclient.WithAuthenticator("webhook", webhook.Name), + conciergeclient.WithAPIGroupSuffix(env.APIGroupSuffix), + ) + require.NoError(t, err) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + resp, err := client.ExchangeToken(ctx, env.TestUser.Token) + requireEventually.NoError(err) + requireEventually.NotNil(resp.Status.ExpirationTimestamp) + requireEventually.InDelta(5*time.Minute, time.Until(resp.Status.ExpirationTimestamp.Time), float64(time.Minute)) + + // Create a client using the certificate and key returned by the token exchange. + validClient := testlib.NewClientsetWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData) + + // Make a version request, which should succeed even without any authorization. + _, err = validClient.Discovery().ServerVersion() + requireEventually.NoError(err) + }, 10*time.Second, 500*time.Millisecond) + }) + } } diff --git a/test/integration/concierge_jwtauthenticator_status_test.go b/test/integration/concierge_jwtauthenticator_status_test.go index 8c77da8ac..bc2d502b9 100644 --- a/test/integration/concierge_jwtauthenticator_status_test.go +++ b/test/integration/concierge_jwtauthenticator_status_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,197 +19,316 @@ import ( "go.pinniped.dev/test/testlib" ) +func TestConciergeJWTAuthenticatorWithExternalCABundleStatusIsUpdatedWhenExternalBundleIsUpdated_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + + if len(env.SupervisorUpstreamOIDC.CABundle) == 0 { + t.Skip("skipping external CA bundle test because env.SupervisorUpstreamOIDC.CABundle is empty") + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + client := testlib.NewKubernetesClientset(t) + + tests := []struct { + name string + caBundleSourceSpecKind authenticationv1alpha1.CertificateAuthorityDataSourceKind + createResourceForCABundle func(t *testing.T, caBundle string) string + updateCABundle func(t *testing.T, resourceName, caBundle string) + }{ + { + name: "for a CA bundle from a ConfigMap", + caBundleSourceSpecKind: authenticationv1alpha1.CertificateAuthorityDataSourceKindConfigMap, + createResourceForCABundle: func(t *testing.T, caBundle string) string { + createdResource := testlib.CreateTestConfigMap(t, env.ConciergeNamespace, "ca-bundle", map[string]string{ + "ca.crt": caBundle, + }) + return createdResource.Name + }, + updateCABundle: func(t *testing.T, resourceName, caBundle string) { + configMap, err := client.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, resourceName, metav1.GetOptions{}) + require.NoError(t, err) + + configMap.Data["ca.crt"] = caBundle + + _, err = client.CoreV1().ConfigMaps(env.ConciergeNamespace).Update(ctx, configMap, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + }, + { + name: "for a CA bundle from a Secret", + caBundleSourceSpecKind: authenticationv1alpha1.CertificateAuthorityDataSourceKindSecret, + createResourceForCABundle: func(t *testing.T, caBundle string) string { + createdResource := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-bundle", corev1.SecretTypeOpaque, map[string]string{ + "ca.crt": caBundle, + }) + return createdResource.Name + }, + updateCABundle: func(t *testing.T, resourceName, caBundle string) { + secret, err := client.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, resourceName, metav1.GetOptions{}) + require.NoError(t, err) + + secret.Data["ca.crt"] = []byte(caBundle) + + _, err = client.CoreV1().Secrets(env.ConciergeNamespace).Update(ctx, secret, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + // Run several times because there is always a chance that the test could pass because the controller + // will resync every 3 minutes even if it does not pay attention to changes in ConfigMaps and Secrets. + for i := range 3 { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + caBundleResourceName := test.createResourceForCABundle(t, env.SupervisorUpstreamOIDC.CABundle) + + authenticator := testlib.CreateTestJWTAuthenticator(ctx, t, authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + Audience: "does-not-matter", + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: test.caBundleSourceSpecKind, + Name: caBundleResourceName, + Key: "ca.crt", + }, + }, + }, authenticationv1alpha1.JWTAuthenticatorPhaseReady) + + t.Logf("created jwtauthenticator %s with CA bundle source %s %s", + authenticator.Name, test.caBundleSourceSpecKind, caBundleResourceName) + + test.updateCABundle(t, caBundleResourceName, "this is not a valid CA bundle value") + testlib.WaitForJWTAuthenticatorStatusPhase(ctx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseError) + + test.updateCABundle(t, caBundleResourceName, env.SupervisorUpstreamOIDC.CABundle) + testlib.WaitForJWTAuthenticatorStatusPhase(ctx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) + }) + } + }) + } +} + +func TestConciergeJWTAuthenticatorStatusShouldBeOverwrittenByControllerAfterAnyManualEdits_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + conciergeClient := testlib.NewConciergeClientset(t) + + // Run several times because there is always a chance that the test could pass because the controller + // will resync every 3 minutes even if it does not pay attention to changes in status. + for i := range 3 { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + authenticator := testlib.CreateTestJWTAuthenticator(ctx, t, authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + Audience: "does-not-matter", + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + }, authenticationv1alpha1.JWTAuthenticatorPhaseReady) + + updatedAuthenticator, err := conciergeClient.AuthenticationV1alpha1().JWTAuthenticators().Get(ctx, authenticator.Name, metav1.GetOptions{}) + require.NoError(t, err) + + updatedAuthenticator.Status.Phase = "Pending" + originalFirstConditionMessage := updatedAuthenticator.Status.Conditions[0].Message + updatedAuthenticator.Status.Conditions[0].Message = "this is a manually edited message that should go away" + _, err = conciergeClient.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updatedAuthenticator, metav1.UpdateOptions{}) + require.NoError(t, err) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + gotAuthenticator, err := conciergeClient.AuthenticationV1alpha1().JWTAuthenticators().Get(ctx, authenticator.Name, metav1.GetOptions{}) + requireEventually.NoError(err) + requireEventually.Equal(authenticationv1alpha1.JWTAuthenticatorPhaseReady, gotAuthenticator.Status.Phase, + "the controller should have changed the phase back to Ready") + requireEventually.Equal(originalFirstConditionMessage, gotAuthenticator.Status.Conditions[0].Message, + "the controller should have changed the message back to the correct value but it didn't") + }, 30*time.Second, 250*time.Millisecond) + }) + } +} + func TestConciergeJWTAuthenticatorStatus_Parallel(t *testing.T) { env := testlib.IntegrationEnv(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) t.Cleanup(cancel) + unusedLocalhostPort := findRecentlyUnusedLocalhostPorts(t, 1)[0] + tests := []struct { - name string - run func(t *testing.T) + name string + spec authenticationv1alpha1.JWTAuthenticatorSpec + wantPhase authenticationv1alpha1.JWTAuthenticatorPhase + wantConditions []metav1.Condition }{ { 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, authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - Audience: "some-fake-audience", - TLS: &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: caBundleString, - }, - }, authenticationv1alpha1.JWTAuthenticatorPhaseReady) - - testlib.WaitForJWTAuthenticatorStatusConditions( - ctx, t, - jwtAuthenticator.Name, - allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0)) + spec: authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + Audience: "some-fake-audience", + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, }, + wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseReady, + wantConditions: allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 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, authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - Audience: "some-fake-audience", - TLS: &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: caBundleString, - }, - }, authenticationv1alpha1.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", - }, - }, - )) + spec: authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + Audience: "some-fake-audience", + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: "invalid base64-encoded data", + }, }, + wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError, + wantConditions: replaceSomeConditions( + allSuccessfulJWTAuthenticatorConditions(true), + []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: "InvalidTLSConfig", + Message: "spec.tls.certificateAuthorityData is invalid: 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, authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - Audience: "some-fake-audience", - // Some random generated cert - // Issuer: C=US, O=Pivotal - // No SAN provided - TLS: &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: caBundleString, - }, - }, authenticationv1alpha1.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", - }, - }, - )) + spec: authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + Audience: "some-fake-audience", + // Some random generated cert + // Issuer: C=US, O=Pivotal + // No SAN provided + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + }, }, + wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError, + wantConditions: replaceSomeConditions( + allSuccessfulJWTAuthenticatorConditions(true), + []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`, + }, + }, + ), }, { 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, authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: fakeIssuerURL, - Audience: "some-fake-audience", - TLS: &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: caBundleString, - }, - }, authenticationv1alpha1.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), - }, - }, - )) + spec: authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: fmt.Sprintf("https://127.0.0.1:%s/some-fake-issuer-path", unusedLocalhostPort), + Audience: "some-fake-audience", + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, }, + wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError, + wantConditions: replaceSomeConditions( + allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0), + []metav1.Condition{ + { + Type: "Ready", + Status: "False", + Reason: "NotReady", + Message: "the JWTAuthenticator is not ready: see other conditions for details", + }, { + Type: "AuthenticatorValid", + Status: "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 "https://127.0.0.1:%s/some-fake-issuer-path/.well-known/openid-configuration": `+ + `dial tcp 127.0.0.1:%s: connect: connection refused`, + unusedLocalhostPort, unusedLocalhostPort), + }, + }, + ), }, } for _, test := range tests { - tt := test - t.Run(tt.name, func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { t.Parallel() - tt.run(t) + + jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, test.spec, test.wantPhase) + testlib.WaitForJWTAuthenticatorStatusConditions(ctx, t, jwtAuthenticator.Name, test.wantConditions) }) } } @@ -243,7 +363,7 @@ func TestConciergeJWTAuthenticatorCRDValidations_Parallel(t *testing.T) { jwtAuthenticator: &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: objectMeta, Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: "https://example.com", + Issuer: env.CLIUpstreamOIDC.Issuer, Audience: "", }, }, @@ -355,9 +475,9 @@ func TestConciergeJWTAuthenticatorCRDValidations_Parallel(t *testing.T) { } func allSuccessfulJWTAuthenticatorConditions(caBundleExists bool) []metav1.Condition { - tlsConfigValidMsg := "no CA bundle specified" + tlsConfigValidMsg := "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image" if caBundleExists { - tlsConfigValidMsg = "successfully parsed specified CA bundle" + tlsConfigValidMsg = "spec.tls is valid: using configured CA bundle" } return []metav1.Condition{{ Type: "AuthenticatorValid", diff --git a/test/integration/concierge_tls_spec_test.go b/test/integration/concierge_tls_spec_test.go new file mode 100644 index 000000000..479ad9ba2 --- /dev/null +++ b/test/integration/concierge_tls_spec_test.go @@ -0,0 +1,737 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package integration + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/here" + "go.pinniped.dev/test/testlib" +) + +// TestTLSSpecKubeBuilderValidationConcierge_Parallel tests kubebuilder validation on the TLSSpec +// in Pinniped concierge CRDs for both WebhookAuthenticators and JWTAuthenticators. +func TestTLSSpecValidationConcierge_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + + ca, err := certauthority.New("pinniped-test", 24*time.Hour) + require.NoError(t, err) + indentedCAPEM := indentForHeredoc(string(ca.Bundle())) + + webhookAuthenticatorYamlTemplate := here.Doc(` + apiVersion: authentication.concierge.%s/v1alpha1 + kind: WebhookAuthenticator + metadata: + name: %s + spec: + endpoint: %s + %s + `) + + jwtAuthenticatorYamlTemplate := here.Doc(` + apiVersion: authentication.concierge.%s/v1alpha1 + kind: JWTAuthenticator + metadata: + name: %s + spec: + issuer: %s + audience: some-audience + %s + `) + + testCases := []struct { + name string + + tlsYAML func(secretOrConfigmapName string) string + + secretOrConfigmapKind string + secretType string + secretOrConfigmapDataYAML string + + wantErrorSnippets []string + wantTLSValidConditionMessage func(namespace string, secretOrConfigmapName string) string + }{ + { + name: "should disallow certificate authority data source with missing name", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.name: Required value`}, + }, + { + name: "should disallow certificate authority data source with empty value for name", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: "" + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.name: Invalid value: "": spec.tls.certificateAuthorityDataSource.name in body should be at least 1 chars long`}, + }, + { + name: "should disallow certificate authority data source with missing key", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: foo + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.key: Required value`}, + }, + { + name: "should disallow certificate authority data source with empty value for key", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: foo + key: "" + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.key: Invalid value: "": spec.tls.certificateAuthorityDataSource.key in body should be at least 1 chars long`}, + }, + { + name: "should disallow certificate authority data source with missing kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Required value`}, + }, + { + name: "should disallow certificate authority data source with empty value for kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: "" + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Unsupported value: "": supported values: "Secret", "ConfigMap"`}, + }, + { + name: "should disallow certificate authority data source with invalid kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: sorcery + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Unsupported value: "sorcery": supported values: "Secret", "ConfigMap"`}, + }, + { + name: "should get error condition when using both fields of the tls spec", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityData: "some CA data" + certificateAuthorityDataSource: + kind: ConfigMap + name: foo + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is invalid: both tls.certificateAuthorityDataSource and tls.certificateAuthorityData provided" + }, + }, + { + name: "should get error condition when certificateAuthorityData is not base64 data", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityData: "this is not base64 encoded" + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4` + }, + }, + { + name: "should get error condition when certificateAuthorityData does not contain PEM data", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityData: "%s" + `, base64.StdEncoding.EncodeToString([]byte("this is not PEM data"))) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")` + }, + }, + { + name: "should get error condition when using a ConfigMap source and the ConfigMap does not exist", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: this-cm-does-not-exist + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: failed to get configmap "%s/this-cm-does-not-exist": configmap "this-cm-does-not-exist" not found`, + namespace) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret does not exist", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: this-secret-does-not-exist + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: failed to get secret "%s/this-secret-does-not-exist": secret "this-secret-does-not-exist" not found`, + namespace) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret is the wrong type", + secretOrConfigmapKind: "Secret", + secretType: "wrong-type", + secretOrConfigmapDataYAML: here.Doc(` + bar: "does not matter for this test" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: secret "%s/%s" of type "wrong-type" cannot be used as a certificate authority data source`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the key does not exist", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + foo: "foo is the wrong key" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" not found in secret "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the key does not exist", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + foo: "foo is the wrong key" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" not found in configmap "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the key has an empty value", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + bar: "" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" has empty value in secret "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the key has an empty value", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + bar: "" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" has empty value in configmap "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret contains data which is not in PEM format", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + bar: "this is not a PEM cert" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" with 22 bytes of data in secret "%s/%s" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the ConfigMap contains data which is not in PEM format", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + bar: "this is not a PEM cert" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" with 22 bytes of data in configmap "%s/%s" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should create a custom resource passing all validations using a Secret source of type Opaque", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Docf(` + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls is valid: using configured CA bundle` + }, + }, + { + name: "should create a custom resource passing all validations using a Secret source of type tls", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeTLS), + secretOrConfigmapDataYAML: here.Docf(` + tls.crt: foo + tls.key: foo + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls is valid: using configured CA bundle` + }, + }, + { + name: "should create a custom resource passing all validations using a ConfigMap source", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Docf(` + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls is valid: using configured CA bundle` + }, + }, + { + name: "should create a custom resource without any tls spec", + tlsYAML: func(secretOrConfigmapName string) string { return "" }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image" + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("apply webhook authenticator", func(t *testing.T) { + resourceName := "test-webhook-authenticator-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.ConciergeNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + yamlBytes := []byte(fmt.Sprintf(webhookAuthenticatorYamlTemplate, + env.APIGroupSuffix, resourceName, env.TestWebhook.Endpoint, + indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName)))) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`webhookauthenticator.authentication.concierge.%s`, env.APIGroupSuffix), + tc.wantErrorSnippets, + "WebhookAuthenticator", + resourceName, + ) + + if tc.wantErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.ConciergeNamespace, + "WebhookAuthenticator", + tc.wantTLSValidConditionMessage(env.ConciergeNamespace, secretOrConfigmapResourceName), + ) + } + }) + + t.Run("apply jwt authenticator", func(t *testing.T) { + _, supervisorIssuer := env.InferSupervisorIssuerURL(t) + + resourceName := "test-jwt-authenticator-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.ConciergeNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + yamlBytes := []byte(fmt.Sprintf(jwtAuthenticatorYamlTemplate, + env.APIGroupSuffix, resourceName, supervisorIssuer, + indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName)))) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`jwtauthenticator.authentication.concierge.%s`, env.APIGroupSuffix), + tc.wantErrorSnippets, + "JWTAuthenticator", + resourceName, + ) + + if tc.wantErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.ConciergeNamespace, + "JWTAuthenticator", + tc.wantTLSValidConditionMessage(env.ConciergeNamespace, secretOrConfigmapResourceName), + ) + } + }) + }) + } +} + +func indentForHeredoc(s string) string { + // Further indent every line except for the first line by four spaces. + // Use four spaces because that's what here.Doc uses. + // Do not indent the first line because the template already indents it. + return strings.ReplaceAll(s, "\n", "\n ") +} + +func requireTLSValidConditionMessageOnResource(t *testing.T, resourceName string, namespace string, resourceType string, wantMessage string) { + t.Helper() + + require.NotEmpty(t, resourceName, "bad test setup: empty resourceName") + require.NotEmpty(t, resourceType, "bad test setup: empty resourceType") + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + conciergeAuthClient := testlib.NewConciergeClientset(t).AuthenticationV1alpha1() + supervisorIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1() + + switch resourceType { + case "JWTAuthenticator": + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := conciergeAuthClient.JWTAuthenticators().Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + case "WebhookAuthenticator": + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := conciergeAuthClient.WebhookAuthenticators().Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + case "OIDCIdentityProvider": + require.NotEmpty(t, namespace, "bad test setup: empty namespace") + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := supervisorIDPClient.OIDCIdentityProviders(namespace).Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + case "LDAPIdentityProvider": + require.NotEmpty(t, namespace, "bad test setup: empty namespace") + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := supervisorIDPClient.LDAPIdentityProviders(namespace).Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + case "ActiveDirectoryIdentityProvider": + require.NotEmpty(t, namespace, "bad test setup: empty namespace") + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := supervisorIDPClient.ActiveDirectoryIdentityProviders(namespace).Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + case "GitHubIdentityProvider": + require.NotEmpty(t, namespace, "bad test setup: empty namespace") + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + got, err := supervisorIDPClient.GitHubIdentityProviders(namespace).Get(ctx, resourceName, metav1.GetOptions{}) + requireEventually.NoError(err) + requireConditionHasMessage(requireEventually, got.Status.Conditions, "TLSConfigurationValid", wantMessage) + }, 10*time.Second, 1*time.Second, "expected resource %s to have condition message %q", resourceName, wantMessage) + default: + require.Failf(t, "unexpected resource type", "type %q", resourceType) + } +} + +func requireConditionHasMessage(assertions *require.Assertions, actualConditions []metav1.Condition, conditionType string, wantMessage string) { + assertions.NotEmpty(actualConditions, "wanted to have conditions but was empty") + for _, c := range actualConditions { + if c.Type == conditionType { + assertions.Equal(wantMessage, c.Message) + return + } + } + assertions.Failf("did not find condition with expected type", + "type %q, actual conditions: %#v", conditionType, actualConditions) +} + +func createSecretOrConfigMapFromData( + t *testing.T, + resourceNameSuffix string, + namespace string, + kind string, + secretType string, + dataYAML string, +) string { + t.Helper() + + if kind == "" { + // Nothing to create. + return "" + } + + require.NotEmpty(t, resourceNameSuffix, "bad test setup: empty resourceNameSuffix") + require.NotEmpty(t, namespace, "bad test setup: empty namespace") + + var resourceYAML string + lowerKind := strings.ToLower(kind) + resourceName := lowerKind + "-" + resourceNameSuffix + + // Further indent every line except for the first line by four spaces. + // Use four spaces because that's what here.Doc uses. + // Do not indent the first line because the template already indents it. + indentedDataYAML := strings.ReplaceAll(dataYAML, "\n", "\n ") + + switch lowerKind { + case "secret": + require.NotEmpty(t, secretType, "bad test setup: empty secret type") + resourceYAML = here.Docf(` + apiVersion: v1 + kind: Secret + metadata: + name: %s + namespace: %s + type: %s + stringData: + %s + `, resourceName, namespace, secretType, indentedDataYAML) + case "configmap": + resourceYAML = here.Docf(` + apiVersion: v1 + kind: ConfigMap + metadata: + name: %s + namespace: %s + data: + %s + `, resourceName, namespace, indentedDataYAML) + default: + require.Failf(t, "unexpected kind in test setup", "kind was %q", kind) + } + + stdOut, stdErr, err := performKubectlApply(t, resourceName, []byte(resourceYAML)) + require.NoErrorf(t, err, + "expected kubectl apply to succeed but got: %s\nstdout: %s\nstderr: %s\nyaml:\n%s", + err, stdOut, stdErr, resourceYAML) + + return resourceName +} + +func performKubectlApply(t *testing.T, resourceName string, yamlBytes []byte) (string, string, error) { + t.Helper() + + yamlFilepath := filepath.Join(t.TempDir(), fmt.Sprintf("test-perform-kubectl-apply-%s.yaml", resourceName)) + + require.NoError(t, os.WriteFile(yamlFilepath, yamlBytes, 0600)) + + // Use --validate=false to disable old client-side validations to avoid getting different error messages in Kube 1.24 and older. + // Note that this also disables validations of unknown and duplicate fields, but that's not what this test is about. + //nolint:gosec // this is test code. + cmd := exec.CommandContext(context.Background(), "kubectl", []string{"apply", "--validate=false", "-f", yamlFilepath}...) + + var stdOut, stdErr bytes.Buffer + cmd.Stdout = &stdOut + cmd.Stderr = &stdErr + err := cmd.Run() + + t.Cleanup(func() { + t.Helper() + //nolint:gosec // this is test code. + require.NoError(t, exec.Command("kubectl", []string{"delete", "--ignore-not-found", "-f", yamlFilepath}...).Run()) + }) + + return stdOut.String(), stdErr.String(), err +} + +func requireKubectlApplyResult( + t *testing.T, + kubectlStdOut string, + kubectlStdErr string, + kubectlErr error, + wantSuccessPrefix string, + wantErrorSnippets []string, + wantResourceType string, + wantResourceName string, +) { + t.Helper() + + if len(wantErrorSnippets) > 0 { + require.Error(t, kubectlErr) + actualErrorString := strings.TrimSuffix(kubectlStdErr, "\n") + for i, snippet := range wantErrorSnippets { + if i == 0 { + snippet = fmt.Sprintf(snippet, wantResourceType, wantResourceName) + } + require.Contains(t, actualErrorString, snippet) + } + } else { + require.Empty(t, kubectlStdErr) + require.Regexp(t, regexp.QuoteMeta(wantSuccessPrefix)+regexp.QuoteMeta(fmt.Sprintf("/%s created\n", wantResourceName)), kubectlStdOut) + require.NoError(t, kubectlErr) + } +} diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go index 99d06034a..c5a9dec3c 100644 --- a/test/integration/concierge_webhookauthenticator_status_test.go +++ b/test/integration/concierge_webhookauthenticator_status_test.go @@ -5,10 +5,13 @@ package integration import ( "context" + "encoding/base64" + "fmt" "testing" "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,8 +19,137 @@ import ( "go.pinniped.dev/test/testlib" ) +func TestConciergeWebhookAuthenticatorWithExternalCABundleStatusIsUpdatedWhenExternalBundleIsUpdated_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + client := testlib.NewKubernetesClientset(t) + + tests := []struct { + name string + caBundleSourceSpecKind authenticationv1alpha1.CertificateAuthorityDataSourceKind + createResourceForCABundle func(t *testing.T, caBundle string) string + updateCABundle func(t *testing.T, resourceName, caBundle string) + }{ + { + name: "for a CA bundle from a ConfigMap", + caBundleSourceSpecKind: authenticationv1alpha1.CertificateAuthorityDataSourceKindConfigMap, + createResourceForCABundle: func(t *testing.T, caBundle string) string { + createdResource := testlib.CreateTestConfigMap(t, env.ConciergeNamespace, "ca-bundle", map[string]string{ + "ca.crt": caBundle, + }) + return createdResource.Name + }, + updateCABundle: func(t *testing.T, resourceName, caBundle string) { + configMap, err := client.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, resourceName, metav1.GetOptions{}) + require.NoError(t, err) + + configMap.Data["ca.crt"] = caBundle + + _, err = client.CoreV1().ConfigMaps(env.ConciergeNamespace).Update(ctx, configMap, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + }, + { + name: "for a CA bundle from a Secret", + caBundleSourceSpecKind: authenticationv1alpha1.CertificateAuthorityDataSourceKindSecret, + createResourceForCABundle: func(t *testing.T, caBundle string) string { + createdResource := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-bundle", corev1.SecretTypeOpaque, map[string]string{ + "ca.crt": caBundle, + }) + return createdResource.Name + }, + updateCABundle: func(t *testing.T, resourceName, caBundle string) { + secret, err := client.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, resourceName, metav1.GetOptions{}) + require.NoError(t, err) + + secret.Data["ca.crt"] = []byte(caBundle) + + _, err = client.CoreV1().Secrets(env.ConciergeNamespace).Update(ctx, secret, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + // Run several times because there is always a chance that the test could pass because the controller + // will resync every 3 minutes even if it does not pay attention to changes in ConfigMaps and Secrets. + for i := range 3 { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + caBundlePEM, err := base64.StdEncoding.DecodeString(env.TestWebhook.TLS.CertificateAuthorityData) + require.NoError(t, err) + + caBundleResourceName := test.createResourceForCABundle(t, string(caBundlePEM)) + + authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, &authenticationv1alpha1.WebhookAuthenticatorSpec{ + Endpoint: env.TestWebhook.Endpoint, + TLS: &authenticationv1alpha1.TLSSpec{ + CertificateAuthorityDataSource: &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: test.caBundleSourceSpecKind, + Name: caBundleResourceName, + Key: "ca.crt", + }, + }, + }, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) + + t.Logf("created webhookauthenticator %s with CA bundle source %s %s", + authenticator.Name, test.caBundleSourceSpecKind, caBundleResourceName) + + test.updateCABundle(t, caBundleResourceName, "this is not a valid CA bundle value") + testlib.WaitForWebhookAuthenticatorStatusPhase(ctx, t, authenticator.Name, authenticationv1alpha1.WebhookAuthenticatorPhaseError) + + test.updateCABundle(t, caBundleResourceName, string(caBundlePEM)) + testlib.WaitForWebhookAuthenticatorStatusPhase(ctx, t, authenticator.Name, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) + }) + } + }) + } +} + +func TestConciergeWebhookAuthenticatorStatusShouldBeOverwrittenByControllerAfterAnyManualEdits_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + conciergeClient := testlib.NewConciergeClientset(t) + + // Run several times because there is always a chance that the test could pass because the controller + // will resync every 3 minutes even if it does not pay attention to changes in status. + for i := range 3 { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, &env.TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady) + + updatedAuthenticator, err := conciergeClient.AuthenticationV1alpha1().WebhookAuthenticators().Get(ctx, authenticator.Name, metav1.GetOptions{}) + require.NoError(t, err) + + updatedAuthenticator.Status.Phase = "Pending" + originalFirstConditionMessage := updatedAuthenticator.Status.Conditions[0].Message + updatedAuthenticator.Status.Conditions[0].Message = "this is a manually edited message that should go away" + _, err = conciergeClient.AuthenticationV1alpha1().WebhookAuthenticators().UpdateStatus(ctx, updatedAuthenticator, metav1.UpdateOptions{}) + require.NoError(t, err) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + gotAuthenticator, err := conciergeClient.AuthenticationV1alpha1().WebhookAuthenticators().Get(ctx, authenticator.Name, metav1.GetOptions{}) + requireEventually.NoError(err) + requireEventually.Equal(authenticationv1alpha1.WebhookAuthenticatorPhaseReady, gotAuthenticator.Status.Phase, + "the controller should have changed the phase back to Ready") + requireEventually.Equal(originalFirstConditionMessage, gotAuthenticator.Status.Conditions[0].Message, + "the controller should have changed the message back to the correct value but it didn't") + }, 30*time.Second, 250*time.Millisecond) + }) + } +} + func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { - testEnv := testlib.IntegrationEnv(t) + env := testlib.IntegrationEnv(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) t.Cleanup(cancel) @@ -31,9 +163,9 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { run func(t *testing.T) }{ { - name: "Basic test to see if the WebhookAuthenticator wakes up or not.", + name: "basic test to see if the WebhookAuthenticator wakes up or not", spec: func() *authenticationv1alpha1.WebhookAuthenticatorSpec { - return &testlib.IntegrationEnv(t).TestWebhook + return &env.TestWebhook }, initialPhase: authenticationv1alpha1.WebhookAuthenticatorPhaseReady, finalConditions: allSuccessfulWebhookAuthenticatorConditions(), @@ -42,7 +174,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { name: "valid spec with invalid CA in TLS config will result in a WebhookAuthenticator that is not ready", spec: func() *authenticationv1alpha1.WebhookAuthenticatorSpec { caBundleString := "invalid base64-encoded data" - webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec := env.TestWebhook.DeepCopy() webhookSpec.TLS = &authenticationv1alpha1.TLSSpec{ CertificateAuthorityData: caBundleString, } @@ -65,8 +197,8 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { }, { Type: "TLSConfigurationValid", Status: "False", - Reason: "InvalidTLSConfiguration", - Message: "invalid TLS configuration: illegal base64 data at input byte 7", + Reason: "InvalidTLSConfig", + Message: "spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 7", }, { Type: "WebhookConnectionValid", Status: "Unknown", @@ -79,7 +211,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { { name: "valid spec with valid CA in TLS config but does not match issuer server will result in a WebhookAuthenticator that is not ready", spec: func() *authenticationv1alpha1.WebhookAuthenticatorSpec { - webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec := env.TestWebhook.DeepCopy() webhookSpec.TLS = &authenticationv1alpha1.TLSSpec{ CertificateAuthorityData: caBundleSomePivotalCA, } @@ -111,7 +243,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) { { name: "invalid with unresponsive endpoint will result in a WebhookAuthenticator that is not ready", spec: func() *authenticationv1alpha1.WebhookAuthenticatorSpec { - webhookSpec := testEnv.TestWebhook.DeepCopy() + webhookSpec := env.TestWebhook.DeepCopy() webhookSpec.TLS = &authenticationv1alpha1.TLSSpec{ CertificateAuthorityData: caBundleSomePivotalCA, } @@ -218,7 +350,7 @@ func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) { { name: "valid authenticator can have empty TLS CertificateAuthorityData", webhookAuthenticator: &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"), + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhookauthenticator"), Spec: authenticationv1alpha1.WebhookAuthenticatorSpec{ Endpoint: "https://localhost/webhook-isnt-actually-here", TLS: &authenticationv1alpha1.TLSSpec{ @@ -231,7 +363,7 @@ func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) { // since the CRD validations do not assess fitness of the value provided name: "valid authenticator can have TLS CertificateAuthorityData string that is an invalid certificate", webhookAuthenticator: &authenticationv1alpha1.WebhookAuthenticator{ - ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"), + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhookauthenticator"), Spec: authenticationv1alpha1.WebhookAuthenticatorSpec{ Endpoint: "https://localhost/webhook-isnt-actually-here", TLS: &authenticationv1alpha1.TLSSpec{ @@ -289,7 +421,7 @@ func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition { Type: "TLSConfigurationValid", Status: "True", Reason: "Success", - Message: "successfully parsed specified CA bundle", + Message: "spec.tls is valid: using configured CA bundle", }, { Type: "WebhookConnectionValid", diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 66aa863a2..7732e4da0 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -70,44 +70,39 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Build pinniped CLI. pinnipedExe := testlib.PinnipedCLIPath(t) - // Infer the downstream issuer URL from the callback associated with the upstream test client registration. - issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) - require.NoError(t, err) - require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) - issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") - t.Logf("testing with downstream issuer URL %s", issuerURL.String()) + issuerURL, _ := env.InferSupervisorIssuerURL(t) // Generate a CA bundle with which to serve this provider. t.Logf("generating test CA") - ca, err := certauthority.New("Downstream Test CA", 1*time.Hour) + federationDomainSelfSignedCA, err := certauthority.New("Downstream Test CA", 1*time.Hour) require.NoError(t, err) // Save that bundle plus the one that signs the upstream issuer, for test purposes. - testCABundlePath := filepath.Join(t.TempDir(), "test-ca.pem") - testCABundlePEM := []byte(string(ca.Bundle()) + "\n" + env.SupervisorUpstreamOIDC.CABundle) - testCABundleBase64 := base64.StdEncoding.EncodeToString(testCABundlePEM) - require.NoError(t, os.WriteFile(testCABundlePath, testCABundlePEM, 0600)) + federationDomainCABundlePath := filepath.Join(t.TempDir(), "test-ca.pem") + federationDomainCABundlePEM := federationDomainSelfSignedCA.Bundle() + require.NoError(t, os.WriteFile(federationDomainCABundlePath, federationDomainCABundlePEM, 0600)) // Use the CA to issue a TLS server cert. t.Logf("issuing test certificate") - tlsCert, err := ca.IssueServerCert([]string{issuerURL.Hostname()}, nil, 1*time.Hour) + federationDomainTLSServingCert, err := federationDomainSelfSignedCA.IssueServerCert( + []string{issuerURL.Hostname()}, nil, 1*time.Hour) require.NoError(t, err) - certPEM, keyPEM, err := certauthority.ToPEM(tlsCert) + federationDomainTLSServingCertPEM, federationDomainTLSServingCertKeyPEM, err := certauthority.ToPEM(federationDomainTLSServingCert) require.NoError(t, err) // Write the serving cert to a secret. - certSecret := testlib.CreateTestSecret(t, + federationDomainTLSServingCertSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "oidc-provider-tls", corev1.SecretTypeTLS, - map[string]string{"tls.crt": string(certPEM), "tls.key": string(keyPEM)}, + map[string]string{"tls.crt": string(federationDomainTLSServingCertPEM), "tls.key": string(federationDomainTLSServingCertKeyPEM)}, ) // Create the downstream FederationDomain and expect it to go into the success status condition. federationDomain := testlib.CreateTestFederationDomain(topSetupCtx, t, supervisorconfigv1alpha1.FederationDomainSpec{ Issuer: issuerURL.String(), - TLS: &supervisorconfigv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + TLS: &supervisorconfigv1alpha1.FederationDomainTLSSpec{SecretName: federationDomainTLSServingCertSecret.Name}, }, supervisorconfigv1alpha1.FederationDomainPhaseError, // in phase error until there is an IDP created ) @@ -115,11 +110,11 @@ 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, authenticationv1alpha1.JWTAuthenticatorSpec{ + defaultJWTAuthenticatorSpec := authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: federationDomain.Spec.Issuer, Audience: clusterAudience, - TLS: &authenticationv1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64}, - }, authenticationv1alpha1.JWTAuthenticatorPhaseError) + TLS: &authenticationv1alpha1.TLSSpec{CertificateAuthorityData: base64.StdEncoding.EncodeToString(federationDomainCABundlePEM)}, + } // 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) { @@ -146,6 +141,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Resource: "namespaces", }) + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) // Create upstream OIDC provider and wait for it to become ready. createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -176,7 +172,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -232,6 +228,22 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Resource: "namespaces", }) + // in this test, use a secret of type TLS to source ca bundle for the JWT authenticator + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": string(federationDomainCABundlePEM), + "tls.crt": "", + "tls.key": "", + }) + jwtAuthnSpec := defaultJWTAuthenticatorSpec.DeepCopy() + jwtAuthnSpec.TLS.CertificateAuthorityData = "" + jwtAuthnSpec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, *jwtAuthnSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) + // Create upstream OIDC provider and wait for it to become ready. createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -262,7 +274,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, "--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups @@ -320,6 +332,20 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Resource: "namespaces", }) + // in this test, use a secret of type opaque to source ca bundle for the JWT authenticator + caSecret := testlib.CreateTestSecret(t, env.ConciergeNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": string(federationDomainCABundlePEM), + }) + jwtAuthnSpec := defaultJWTAuthenticatorSpec.DeepCopy() + jwtAuthnSpec.TLS.CertificateAuthorityData = "" + jwtAuthnSpec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, *jwtAuthnSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) + // Create upstream OIDC provider and wait for it to become ready. createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -351,7 +377,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", "--oidc-skip-listen", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -444,6 +470,20 @@ func TestE2EFullIntegration_Browser(t *testing.T) { } } + // in this test, use a configmap to source ca bundle for the JWT authenticator + caConfigMap := testlib.CreateTestConfigMap(t, env.ConciergeNamespace, "ca-cert", + map[string]string{ + "ca.crt": string(federationDomainCABundlePEM), + }) + jwtAuthnSpec := defaultJWTAuthenticatorSpec.DeepCopy() + jwtAuthnSpec.TLS.CertificateAuthorityData = "" + jwtAuthnSpec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caConfigMap.Name, + Key: "ca.crt", + } + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, *jwtAuthnSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) + // Create upstream OIDC provider and wait for it to become ready. createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -475,7 +515,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", "--oidc-skip-listen", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -574,6 +614,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Resource: "namespaces", }) + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) // Create upstream OIDC provider and wait for it to become ready. createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -607,7 +648,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-browser", "--oidc-skip-listen", "--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -647,6 +688,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { tempDir := t.TempDir() // per-test tmp dir to avoid sharing files between tests + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) // Create upstream OIDC provider and wait for it to become ready. oidcIdentityProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -686,7 +728,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--upstream-identity-provider-name", oidcIdentityProvider.Name, "--upstream-identity-provider-type", "oidc", "--upstream-identity-provider-flow", "cli_password", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -728,6 +770,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -787,6 +830,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -850,6 +894,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -921,6 +966,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -980,6 +1026,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -1053,6 +1100,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -1067,7 +1115,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1108,6 +1156,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -1122,7 +1171,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1163,6 +1212,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -1177,7 +1227,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1254,7 +1304,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }, }, idpv1alpha1.GitHubPhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) - testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1266,7 +1316,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, + "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -1325,8 +1375,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedDownstreamOIDCGroups = append(expectedDownstreamOIDCGroups, downstreamPrefix+g) } + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseError) createdLDAPProvider := setupClusterForEndToEndLDAPTest(t, expectedDownstreamLDAPUsername, env) - // Having one IDP should put the FederationDomain into a ready state. testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) @@ -1646,10 +1696,9 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedDownstreamOIDCGroups := env.SupervisorUpstreamOIDC.ExpectedGroups createdLDAPProvider := setupClusterForEndToEndLDAPTest(t, expectedDownstreamLDAPUsername, env) - // Having one IDP should put the FederationDomain into a ready state. testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) - testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authenticationv1alpha1.JWTAuthenticatorPhaseReady) + authenticator := testlib.CreateTestJWTAuthenticator(testCtx, t, defaultJWTAuthenticatorSpec, authenticationv1alpha1.JWTAuthenticatorPhaseReady) // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. testlib.CreateTestClusterRoleBinding(t, diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 571adbdfe..a166e552d 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -452,7 +452,7 @@ 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 := 289 + totalExpectedAPIFields := 313 // 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 @@ -552,7 +552,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { addSuffix("webhookauthenticators.authentication.concierge"): { "v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{ {Name: "Endpoint", Type: "string", JSONPath: ".spec.endpoint"}, - // Note that WebhookAuthenticators have a status type, but no controller currently sets the status, so we don't show it. + {Name: "Status", Type: "string", JSONPath: ".status.phase"}, {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, }, }, @@ -560,7 +560,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { "v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{ {Name: "Issuer", Type: "string", JSONPath: ".spec.issuer"}, {Name: "Audience", Type: "string", JSONPath: ".spec.audience"}, - // Note that JWTAuthenticators have a status type, but no controller currently sets the status, so we don't show it. + {Name: "Status", Type: "string", JSONPath: ".status.phase"}, {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, }, }, diff --git a/test/integration/supervisor_github_idp_test.go b/test/integration/supervisor_github_idp_test.go index b250cdf3f..3a135dbc1 100644 --- a/test/integration/supervisor_github_idp_test.go +++ b/test/integration/supervisor_github_idp_test.go @@ -410,7 +410,7 @@ func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) { Type: "TLSConfigurationValid", Status: metav1.ConditionTrue, Reason: "Success", - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + Message: "spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image", }, }, }, @@ -479,7 +479,7 @@ func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) { Type: "TLSConfigurationValid", Status: metav1.ConditionTrue, Reason: "Success", - Message: `spec.githubAPI.tls.certificateAuthorityData is valid`, + Message: `spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image`, }, }, }, @@ -686,7 +686,7 @@ func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) { Type: "TLSConfigurationValid", Status: metav1.ConditionTrue, Reason: "Success", - Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + Message: "spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image", }, }) } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 442985c8a..c9b14fa4d 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -178,6 +178,29 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) } + skipExternalCABundleOIDCTestsWhenCABundleIsEmpty := func(t *testing.T) { + t.Helper() + if len(env.SupervisorUpstreamOIDC.CABundle) == 0 { + t.Skip("skipping external CA bundle test because env.SupervisorUpstreamOIDC.CABundle is empty") + } + } + + skipExternalCABundleLDAPTestsWhenCABundleIsEmpty := func(t *testing.T) { + t.Helper() + skipLDAPTests(t) + if len(env.SupervisorUpstreamLDAP.CABundle) == 0 { + t.Skip("skipping external CA bundle test because env.SupervisorUpstreamLDAP.CABundle is empty") + } + } + + skipExternalCABundleActiveDirectoryTestsWhenCABundleIsEmpty := func(t *testing.T) { + t.Helper() + skipActiveDirectoryTests(t) + if len(env.SupervisorUpstreamActiveDirectory.CABundle) == 0 { + t.Skip("skipping external CA bundle test because env.SupervisorUpstreamActiveDirectory.CABundle is empty") + } + } + basicOIDCIdentityProviderSpec := func() idpv1alpha1.OIDCIdentityProviderSpec { return idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -216,12 +239,13 @@ func TestSupervisorLogin_Browser(t *testing.T) { adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, spec, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( + expectedActiveDirectoryConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, spec.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, secret.Name, secret.ResourceVersion, ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, + adIDP, expectedActiveDirectoryConnectionValidMessage, env.SupervisorUpstreamActiveDirectory.CABundle != "") return adIDP, secret } @@ -269,12 +293,13 @@ func TestSupervisorLogin_Browser(t *testing.T) { ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, spec, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( + expectedLDAPConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, spec.Host, env.SupervisorUpstreamLDAP.BindUsername, secret.Name, secret.ResourceVersion, ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) + requireSuccessfulLDAPIdentityProviderConditions(t, + ldapIDP, expectedLDAPConnectionValidMessage, len(env.SupervisorUpstreamLDAP.CABundle) != 0) return ldapIDP, secret } @@ -301,7 +326,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + "$" - // The downstream ID token Subject should be in the the same format as LDAP above, but with AD-specific values. + // The downstream ID token Subject should be in the same format as LDAP above, but with AD-specific values. expectedIDTokenSubjectRegexForUpstreamAD := "^" + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + @@ -338,6 +363,121 @@ func TestSupervisorLogin_Browser(t *testing.T) { // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, }, + { + name: "oidc IDP using secrets of type opaque to source ca bundle with default username and groups claim settings", + maybeSkip: skipExternalCABundleOIDCTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idpSpec := basicOIDCIdentityProviderSpec() + caData, err := base64.StdEncoding.DecodeString(idpSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": string(caData), + }) + idpSpec.TLS.CertificateAuthorityData = "" + idpSpec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + return testlib.CreateTestOIDCIdentityProvider(t, idpSpec, idpv1alpha1.PhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + pinnipedSessionData := pinnipedSession.Custom + pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, + // the ID token Username should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, + }, + { + name: "oidc IDP using secrets of type TLS to source ca bundle with default username and groups claim settings", + maybeSkip: skipExternalCABundleOIDCTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idpSpec := basicOIDCIdentityProviderSpec() + caData, err := base64.StdEncoding.DecodeString(idpSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": string(caData), + "tls.crt": "", + "tls.key": "", + }) + idpSpec.TLS.CertificateAuthorityData = "" + idpSpec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + return testlib.CreateTestOIDCIdentityProvider(t, idpSpec, idpv1alpha1.PhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + pinnipedSessionData := pinnipedSession.Custom + pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, + // the ID token Username should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, + }, + { + name: "oidc IDP using configmaps to source ca bundle with default username and groups claim settings", + maybeSkip: skipExternalCABundleOIDCTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idpSpec := basicOIDCIdentityProviderSpec() + caData, err := base64.StdEncoding.DecodeString(idpSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + caConfigMap := testlib.CreateTestConfigMap(t, env.SupervisorNamespace, "ca-cert", map[string]string{ + "ca.crt": string(caData), + }) + idpSpec.TLS.CertificateAuthorityData = "" + idpSpec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caConfigMap.Name, + Key: "ca.crt", + } + return testlib.CreateTestOIDCIdentityProvider(t, idpSpec, idpv1alpha1.PhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + pinnipedSessionData := pinnipedSession.Custom + pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, + // the ID token Username should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, + }, + + { + name: "oidc IDP using secrets of type opaque to source ca bundle with default username and groups claim settings", + maybeSkip: skipExternalCABundleOIDCTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idpSpec := basicOIDCIdentityProviderSpec() + caData, err := base64.StdEncoding.DecodeString(idpSpec.TLS.CertificateAuthorityData) + require.NoError(t, err) + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": string(caData), + }) + idpSpec.TLS.CertificateAuthorityData = "" + idpSpec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + return testlib.CreateTestOIDCIdentityProvider(t, idpSpec, idpv1alpha1.PhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + pinnipedSessionData := pinnipedSession.Custom + pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, + // the ID token Username should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, + }, + { name: "oidc with custom username and groups claim settings", maybeSkip: skipNever, @@ -534,6 +674,155 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, + { + name: "ldap IDP using secrets of type opaque to source ca bundle and with email as username and groups names as DNs and using an LDAP provider which supports TLS", + maybeSkip: skipExternalCABundleLDAPTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": env.SupervisorUpstreamLDAP.CABundle, + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { + // Even if we update this group to the some names that did not come from the LDAP server, + // we expect that it will return to the real groups from the LDAP server after we refresh. + initialGroupMembership := []string{"some-wrong-group", "some-other-group"} + sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session + sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session + return env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.LDAP.UserDN) + fositeSessionData := pinnipedSession.Fosite + fositeSessionData.Claims.Subject = "not-right" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, + }, + { + name: "ldap IDP using secrets of type TLS to source ca bundle and with email as username and groups names as DNs and using an LDAP provider which supports TLS", + maybeSkip: skipExternalCABundleLDAPTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": env.SupervisorUpstreamLDAP.CABundle, + "tls.crt": "", + "tls.key": "", + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { + // Even if we update this group to the some names that did not come from the LDAP server, + // we expect that it will return to the real groups from the LDAP server after we refresh. + initialGroupMembership := []string{"some-wrong-group", "some-other-group"} + sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session + sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session + return env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.LDAP.UserDN) + fositeSessionData := pinnipedSession.Fosite + fositeSessionData.Claims.Subject = "not-right" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, + }, + { + name: "ldap IDP using configmaps to source ca bundle and with email as username and groups names as DNs and using an LDAP provider which supports TLS", + maybeSkip: skipExternalCABundleLDAPTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + caConfigMap := testlib.CreateTestConfigMap(t, env.SupervisorNamespace, "ca-cert", + map[string]string{ + "ca.crt": env.SupervisorUpstreamLDAP.CABundle, + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caConfigMap.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { + // Even if we update this group to the some names that did not come from the LDAP server, + // we expect that it will return to the real groups from the LDAP server after we refresh. + initialGroupMembership := []string{"some-wrong-group", "some-other-group"} + sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session + sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session + return env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.LDAP.UserDN) + fositeSessionData := pinnipedSession.Fosite + fositeSessionData.Claims.Subject = "not-right" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, + }, { name: "ldap using posix groups by using the UserAttributeForFilter option to adjust the group search filter behavior", maybeSkip: skipLDAPTests, @@ -837,7 +1126,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { updatedSecret, err := client.CoreV1().Secrets(env.SupervisorNamespace).Update(ctx, secret, metav1.UpdateOptions{}) require.NoError(t, err) - expectedMsg := fmt.Sprintf( + expectedLDAPConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, updatedSecret.Name, updatedSecret.ResourceVersion, @@ -848,7 +1137,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer cancel() idp, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, idp, expectedMsg) + requireEventuallySuccessfulLDAPIdentityProviderConditions(t, + requireEventually, idp, expectedLDAPConnectionValidMessage, len(env.SupervisorUpstreamLDAP.CABundle) != 0) }, time.Minute, 500*time.Millisecond) return idp.Name }, @@ -903,7 +1193,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, }, metav1.CreateOptions{}) require.NoError(t, err) - expectedMsg := fmt.Sprintf( + expectedLDAPConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, recreatedSecret.Name, recreatedSecret.ResourceVersion, @@ -914,7 +1204,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer cancel() idp, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, idp, expectedMsg) + requireEventuallySuccessfulLDAPIdentityProviderConditions(t, + requireEventually, idp, expectedLDAPConnectionValidMessage, len(env.SupervisorUpstreamLDAP.CABundle) != 0) }, time.Minute, 500*time.Millisecond) return idp.Name }, @@ -969,6 +1260,128 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, + { + name: "active directory IDP using secret of type opaque to source ca bundle with all default options", + maybeSkip: skipExternalCABundleActiveDirectoryTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createActiveDirectoryIdentityProvider(t, func(spec *idpv1alpha1.ActiveDirectoryIdentityProviderSpec) { + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeOpaque, + map[string]string{ + "ca.crt": env.SupervisorUpstreamActiveDirectory.CABundle, + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) + customSessionData.Username = "not-the-same" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, + }, + { + name: "active directory IDP using secret of type TLS to source ca bundle with all default options", + maybeSkip: skipExternalCABundleActiveDirectoryTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createActiveDirectoryIdentityProvider(t, func(spec *idpv1alpha1.ActiveDirectoryIdentityProviderSpec) { + caSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ca-cert", corev1.SecretTypeTLS, + map[string]string{ + "ca.crt": env.SupervisorUpstreamActiveDirectory.CABundle, + "tls.crt": "", + "tls.key": "", + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "Secret", + Name: caSecret.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) + customSessionData.Username = "not-the-same" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, + }, + { + name: "active directory IDP using configmaps to source ca bundle with all default options", + maybeSkip: skipExternalCABundleActiveDirectoryTestsWhenCABundleIsEmpty, + createIDP: func(t *testing.T) string { + idp, _ := createActiveDirectoryIdentityProvider(t, func(spec *idpv1alpha1.ActiveDirectoryIdentityProviderSpec) { + caConfigMap := testlib.CreateTestConfigMap(t, env.SupervisorNamespace, "ca-cert", + map[string]string{ + "ca.crt": env.SupervisorUpstreamActiveDirectory.CABundle, + }) + spec.TLS.CertificateAuthorityData = "" + spec.TLS.CertificateAuthorityDataSource = &idpv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: "ConfigMap", + Name: caConfigMap.Name, + Key: "ca.crt", + } + }) + return idp.Name + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorizationUsingCLIPasswordFlow(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) + customSessionData.Username = "not-the-same" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, + }, { name: "active directory with custom options", maybeSkip: skipActiveDirectoryTests, @@ -1072,7 +1485,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { updatedSecret, err := client.CoreV1().Secrets(env.SupervisorNamespace).Update(ctx, secret, metav1.UpdateOptions{}) require.NoError(t, err) - expectedMsg := fmt.Sprintf( + expectedActiveDirectoryConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, updatedSecret.Name, updatedSecret.ResourceVersion, @@ -1083,7 +1496,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer cancel() idp, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, idp, expectedMsg) + requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, + requireEventually, idp, expectedActiveDirectoryConnectionValidMessage, len(env.SupervisorUpstreamActiveDirectory.CABundle) != 0) }, time.Minute, 500*time.Millisecond) return idp.Name }, @@ -1139,7 +1553,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, metav1.CreateOptions{}) require.NoError(t, err) - expectedMsg := fmt.Sprintf( + expectedActiveDirectoryConnectionValidMessage := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, recreatedSecret.Name, recreatedSecret.ResourceVersion, @@ -1150,7 +1564,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer cancel() idp, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, idp, expectedMsg) + requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, + requireEventually, idp, expectedActiveDirectoryConnectionValidMessage, len(env.SupervisorUpstreamActiveDirectory.CABundle) != 0) }, time.Minute, 500*time.Millisecond) return idp.Name }, @@ -2403,48 +2818,53 @@ func wantGroupsInAdditionalClaimsIfGroupsExist(additionalClaims map[string]any, return additionalClaims } -func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv1alpha1.LDAPIdentityProvider, expectedLDAPConnectionValidMessage string) { - require.Len(t, ldapIDP.Status.Conditions, 3) +func requireSuccessfulLDAPIdentityProviderConditions( + t *testing.T, + ldapIDP *idpv1alpha1.LDAPIdentityProvider, + expectedLDAPConnectionValidMessage string, + caBundleConfigured bool, +) { + requireEventuallySuccessfulLDAPIdentityProviderConditions(t, + require.New(t), ldapIDP, expectedLDAPConnectionValidMessage, caBundleConfigured) +} - conditionsSummary := [][]string{} - for _, condition := range ldapIDP.Status.Conditions { - conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) - t.Logf("Saw LDAPIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s", - condition.Type, string(condition.Status), condition.Reason, condition.Message) - switch condition.Type { - case "BindSecretValid": - require.Equal(t, "loaded bind secret", condition.Message) - case "TLSConfigurationValid": - require.Equal(t, "loaded TLS configuration", condition.Message) - case "LDAPConnectionValid": - require.Equal(t, expectedLDAPConnectionValidMessage, condition.Message) - } - } +func requireSuccessfulActiveDirectoryIdentityProviderConditions( + t *testing.T, + adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, + expectedActiveDirectoryConnectionValidMessage string, + caBundleConfigured bool, +) { + requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, + require.New(t), adIDP, expectedActiveDirectoryConnectionValidMessage, caBundleConfigured) +} - require.ElementsMatch(t, [][]string{ +func requireEventuallySuccessfulLDAPIdentityProviderConditions( + t *testing.T, + assertions *require.Assertions, + ldapIDP *idpv1alpha1.LDAPIdentityProvider, + expectedLDAPConnectionValidMessage string, + caBundleConfigured bool, +) { + t.Helper() + assertions.Len(ldapIDP.Status.Conditions, 3) + + assertions.ElementsMatch([][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, {"LDAPConnectionValid", "True", "Success"}, - }, conditionsSummary) + }, conditionsSummaryFromActualConditions(t, + assertions, ldapIDP.Status.Conditions, caBundleConfigured, expectedLDAPConnectionValidMessage)) } -func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { - require.Len(t, adIDP.Status.Conditions, 4) - - conditionsSummary := [][]string{} - for _, condition := range adIDP.Status.Conditions { - conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) - t.Logf("Saw ActiveDirectoryIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s", - condition.Type, string(condition.Status), condition.Reason, condition.Message) - switch condition.Type { - case "BindSecretValid": - require.Equal(t, "loaded bind secret", condition.Message) - case "TLSConfigurationValid": - require.Equal(t, "loaded TLS configuration", condition.Message) - case "LDAPConnectionValid": - require.Equal(t, expectedActiveDirectoryConnectionValidMessage, condition.Message) - } - } +func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions( + t *testing.T, + assertions *require.Assertions, + adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, + expectedActiveDirectoryConnectionValidMessage string, + caBundleConfigured bool, +) { + t.Helper() + assertions.Len(adIDP.Status.Conditions, 4) expectedUserSearchReason := "" if adIDP.Spec.UserSearch.Base == "" || adIDP.Spec.GroupSearch.Base == "" { @@ -2453,72 +2873,41 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad expectedUserSearchReason = "UsingConfigurationFromSpec" } - require.ElementsMatch(t, [][]string{ + assertions.ElementsMatch([][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, {"LDAPConnectionValid", "True", "Success"}, {"SearchBaseFound", "True", expectedUserSearchReason}, - }, conditionsSummary) + }, conditionsSummaryFromActualConditions(t, + assertions, adIDP.Status.Conditions, caBundleConfigured, expectedActiveDirectoryConnectionValidMessage)) } -func requireEventuallySuccessfulLDAPIdentityProviderConditions(t *testing.T, requireEventually *require.Assertions, ldapIDP *idpv1alpha1.LDAPIdentityProvider, expectedLDAPConnectionValidMessage string) { - t.Helper() - requireEventually.Len(ldapIDP.Status.Conditions, 3) - +func conditionsSummaryFromActualConditions( + t *testing.T, + assertions *require.Assertions, + conditions []metav1.Condition, + caBundleConfigured bool, + expectedLDAPConnectionValidMessage string, +) [][]string { conditionsSummary := [][]string{} - for _, condition := range ldapIDP.Status.Conditions { + for _, condition := range conditions { conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) - t.Logf("Saw ActiveDirectoryIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s", + t.Logf("Saw identity provider with Status.Condition Type=%s Status=%s Reason=%s Message=%s", condition.Type, string(condition.Status), condition.Reason, condition.Message) switch condition.Type { case "BindSecretValid": - requireEventually.Equal("loaded bind secret", condition.Message) + assertions.Equal("loaded bind secret", condition.Message) case "TLSConfigurationValid": - requireEventually.Equal("loaded TLS configuration", condition.Message) + if caBundleConfigured { + assertions.Equal("spec.tls is valid: using configured CA bundle", condition.Message) + } else { + assertions.Equal("spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", condition.Message) + } case "LDAPConnectionValid": - requireEventually.Equal(expectedLDAPConnectionValidMessage, condition.Message) + assertions.Equal(expectedLDAPConnectionValidMessage, condition.Message) } } - - requireEventually.ElementsMatch([][]string{ - {"BindSecretValid", "True", "Success"}, - {"TLSConfigurationValid", "True", "Success"}, - {"LDAPConnectionValid", "True", "Success"}, - }, conditionsSummary) -} - -func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, requireEventually *require.Assertions, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { - t.Helper() - requireEventually.Len(adIDP.Status.Conditions, 4) - - conditionsSummary := [][]string{} - for _, condition := range adIDP.Status.Conditions { - conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) - t.Logf("Saw ActiveDirectoryIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s", - condition.Type, string(condition.Status), condition.Reason, condition.Message) - switch condition.Type { - case "BindSecretValid": - requireEventually.Equal("loaded bind secret", condition.Message) - case "TLSConfigurationValid": - requireEventually.Equal("loaded TLS configuration", condition.Message) - case "LDAPConnectionValid": - requireEventually.Equal(expectedActiveDirectoryConnectionValidMessage, condition.Message) - } - } - - expectedUserSearchReason := "" - if adIDP.Spec.UserSearch.Base == "" || adIDP.Spec.GroupSearch.Base == "" { - expectedUserSearchReason = "Success" - } else { - expectedUserSearchReason = "UsingConfigurationFromSpec" - } - - requireEventually.ElementsMatch([][]string{ - {"BindSecretValid", "True", "Success"}, - {"TLSConfigurationValid", "True", "Success"}, - {"LDAPConnectionValid", "True", "Success"}, - {"SearchBaseFound", "True", expectedUserSearchReason}, - }, conditionsSummary) + return conditionsSummary } func testSupervisorLogin( @@ -2549,12 +2938,7 @@ func testSupervisorLogin( ctx, cancel := context.WithTimeout(context.Background(), 7*time.Minute) defer cancel() - // Infer the downstream issuer URL from the callback associated with the upstream test client registration. - issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) - require.NoError(t, err) - require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) - issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") - t.Logf("testing with downstream issuer URL %s", issuerURL.String()) + issuerURL, _ := env.InferSupervisorIssuerURL(t) // Generate a CA bundle with which to serve this provider. t.Logf("generating test CA") diff --git a/test/integration/supervisor_tls_spec_test.go b/test/integration/supervisor_tls_spec_test.go new file mode 100644 index 000000000..89f76b505 --- /dev/null +++ b/test/integration/supervisor_tls_spec_test.go @@ -0,0 +1,674 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package integration + +import ( + "encoding/base64" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/here" + "go.pinniped.dev/test/testlib" +) + +// TestTLSSpecKubeBuilderValidationSupervisor_Parallel tests kubebuilder validation +// on the TLSSpec in Pinniped supervisor CRDs using OIDCIdentityProvider as an example. +func TestTLSSpecValidationSupervisor_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + + ca, err := certauthority.New("pinniped-test", 24*time.Hour) + require.NoError(t, err) + indentedCAPEM := indentForHeredoc(string(ca.Bundle())) + + oidcIDPTemplate := here.Doc(` + apiVersion: idp.supervisor.%s/v1alpha1 + kind: OIDCIdentityProvider + metadata: + name: %s + namespace: %s + spec: + issuer: %s + authorizationConfig: + additionalScopes: [offline_access, email] + allowPasswordGrant: true + client: + secretName: foo-bar-client-credentials + %s + `) + + ldapIDPTemplate := here.Doc(` + apiVersion: idp.supervisor.%s/v1alpha1 + kind: LDAPIdentityProvider + metadata: + name: %s + namespace: %s + spec: + host: %s + bind: + secretName: foo-bar-bind-credentials + userSearch: + base: foo + attributes: + username: bar + uid: baz + %s + `) + + activeDirectoryIDPTemplate := here.Doc(` + apiVersion: idp.supervisor.%s/v1alpha1 + kind: ActiveDirectoryIdentityProvider + metadata: + name: %s + namespace: %s + spec: + host: %s + bind: + secretName: foo-bar-bind-credentials + %s + `) + + githubIDPTemplate := here.Doc(` + apiVersion: idp.supervisor.%s/v1alpha1 + kind: GitHubIdentityProvider + metadata: + name: %s + namespace: %s + spec: + allowAuthentication: + organizations: + policy: AllGitHubUsers + client: + secretName: does-not-matter + githubAPI: + %s + `) + + testCases := []struct { + name string + + tlsYAML func(secretOrConfigmapName string) string + + secretOrConfigmapKind string + secretType string + secretOrConfigmapDataYAML string + + wantErrorSnippets []string + wantGitHubErrorSnippets []string + wantTLSValidConditionMessage func(namespace string, secretOrConfigmapName string) string + }{ + { + name: "should disallow certificate authority data source with missing name", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.name: Required value`}, + wantGitHubErrorSnippets: []string{ + `The %s "%s" is invalid:`, + "spec.githubAPI.tls.certificateAuthorityDataSource.name: Required value", + }, + }, + { + name: "should disallow certificate authority data source with empty value for name", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: "" + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.name: Invalid value: "": spec.tls.certificateAuthorityDataSource.name in body should be at least 1 chars long`}, + wantGitHubErrorSnippets: []string{`The %s "%s" is invalid: spec.githubAPI.tls.certificateAuthorityDataSource.name: Invalid value: "": spec.githubAPI.tls.certificateAuthorityDataSource.name in body should be at least 1 chars long`}, + }, + { + name: "should disallow certificate authority data source with missing key", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: foo + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.key: Required value`}, + wantGitHubErrorSnippets: []string{ + `The %s "%s" is invalid:`, + "spec.githubAPI.tls.certificateAuthorityDataSource.key: Required value", + }, + }, + { + name: "should disallow certificate authority data source with empty value for key", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: foo + key: "" + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.key: Invalid value: "": spec.tls.certificateAuthorityDataSource.key in body should be at least 1 chars long`}, + wantGitHubErrorSnippets: []string{`The %s "%s" is invalid: spec.githubAPI.tls.certificateAuthorityDataSource.key: Invalid value: "": spec.githubAPI.tls.certificateAuthorityDataSource.key in body should be at least 1 chars long`}, + }, + { + name: "should disallow certificate authority data source with missing kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Required value`}, + wantGitHubErrorSnippets: []string{ + `The %s "%s" is invalid:`, + "spec.githubAPI.tls.certificateAuthorityDataSource.kind: Required value", + }, + }, + { + name: "should disallow certificate authority data source with empty value for kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: "" + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Unsupported value: "": supported values: "Secret", "ConfigMap"`}, + wantGitHubErrorSnippets: []string{ + `The %s "%s" is invalid:`, + `spec.githubAPI.tls.certificateAuthorityDataSource.kind: Unsupported value: "": supported values: "Secret", "ConfigMap"`, + }, + }, + { + name: "should disallow certificate authority data source with invalid kind", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: sorcery + name: foo + key: bar + `) + }, + wantErrorSnippets: []string{`The %s "%s" is invalid: spec.tls.certificateAuthorityDataSource.kind: Unsupported value: "sorcery": supported values: "Secret", "ConfigMap"`}, + wantGitHubErrorSnippets: []string{ + `The %s "%s" is invalid:`, + `spec.githubAPI.tls.certificateAuthorityDataSource.kind: Unsupported value: "sorcery": supported values: "Secret", "ConfigMap"`, + }, + }, + { + name: "should get error condition when using both fields of the tls spec", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityData: "some CA data" + certificateAuthorityDataSource: + kind: ConfigMap + name: foo + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is invalid: both tls.certificateAuthorityDataSource and tls.certificateAuthorityData provided" + }, + }, + { + name: "should get error condition when certificateAuthorityData is not base64 data", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityData: "this is not base64 encoded" + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls.certificateAuthorityData is invalid: illegal base64 data at input byte 4` + }, + }, + { + name: "should get error condition when certificateAuthorityData does not contain PEM data", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityData: "%s" + `, base64.StdEncoding.EncodeToString([]byte("this is not PEM data"))) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls.certificateAuthorityData is invalid: no base64-encoded PEM certificates found in 28 bytes of data (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")` + }, + }, + { + name: "should get error condition when using a ConfigMap source and the ConfigMap does not exist", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: this-cm-does-not-exist + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: failed to get configmap "%s/this-cm-does-not-exist": configmap "this-cm-does-not-exist" not found`, + namespace) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret does not exist", + tlsYAML: func(secretOrConfigmapName string) string { + return here.Doc(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: this-secret-does-not-exist + key: bar + `) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: failed to get secret "%s/this-secret-does-not-exist": secret "this-secret-does-not-exist" not found`, + namespace) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret is the wrong type", + secretOrConfigmapKind: "Secret", + secretType: "wrong-type", + secretOrConfigmapDataYAML: here.Doc(` + bar: "does not matter for this test" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: secret "%s/%s" of type "wrong-type" cannot be used as a certificate authority data source`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the key does not exist", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + foo: "foo is the wrong key" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" not found in secret "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the key does not exist", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + foo: "foo is the wrong key" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" not found in configmap "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the key has an empty value", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + bar: "" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" has empty value in secret "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the key has an empty value", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + bar: "" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" has empty value in configmap "%s/%s"`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a Secret source and the Secret contains data which is not in PEM format", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Doc(` + bar: "this is not a PEM cert" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" with 22 bytes of data in secret "%s/%s" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should get error condition when using a ConfigMap source and the ConfigMap contains data which is not in PEM format", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Doc(` + bar: "this is not a PEM cert" + `), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return fmt.Sprintf( + `spec.tls.certificateAuthorityDataSource is invalid: key "bar" with 22 bytes of data in configmap "%s/%s" is not a PEM-encoded certificate (PEM certificates must begin with "-----BEGIN CERTIFICATE-----")`, + namespace, secretOrConfigmapName) + }, + }, + { + name: "should create a custom resource passing all validations using a Secret source of type Opaque", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeOpaque), + secretOrConfigmapDataYAML: here.Docf(` + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantGitHubErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is valid: using configured CA bundle" + }, + }, + { + name: "should create a custom resource passing all validations using a Secret source of type tls", + secretOrConfigmapKind: "Secret", + secretType: string(corev1.SecretTypeTLS), + secretOrConfigmapDataYAML: here.Docf(` + tls.crt: foo + tls.key: foo + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: Secret + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantGitHubErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is valid: using configured CA bundle" + }, + }, + { + name: "should create a custom resource passing all validations using a ConfigMap source", + secretOrConfigmapKind: "ConfigMap", + secretOrConfigmapDataYAML: here.Docf(` + bar: | + %s + `, indentedCAPEM), + tlsYAML: func(secretOrConfigmapName string) string { + return here.Docf(` + tls: + certificateAuthorityDataSource: + kind: ConfigMap + name: %s + key: bar + `, secretOrConfigmapName) + }, + wantErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return `spec.tls is valid: using configured CA bundle` + }, + }, + { + name: "should create a custom resource without any tls spec", + tlsYAML: func(secretOrConfigmapName string) string { return "" }, + wantErrorSnippets: nil, + wantGitHubErrorSnippets: nil, + wantTLSValidConditionMessage: func(namespace string, secretOrConfigmapName string) string { + return "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image" + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("apply OIDC IDP", func(t *testing.T) { + resourceName := "test-oidc-idp-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.SupervisorNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + yamlBytes := []byte(fmt.Sprintf(oidcIDPTemplate, + env.APIGroupSuffix, resourceName, env.SupervisorNamespace, env.SupervisorUpstreamOIDC.Issuer, + indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName)))) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`oidcidentityprovider.idp.supervisor.%s`, env.APIGroupSuffix), + tc.wantErrorSnippets, + "OIDCIdentityProvider", + resourceName, + ) + + if tc.wantErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.SupervisorNamespace, + "OIDCIdentityProvider", + tc.wantTLSValidConditionMessage(env.SupervisorNamespace, secretOrConfigmapResourceName), + ) + } + }) + + t.Run("apply LDAP IDP", func(t *testing.T) { + resourceName := "test-ldap-idp-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.SupervisorNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + yamlBytes := []byte(fmt.Sprintf(ldapIDPTemplate, + env.APIGroupSuffix, resourceName, env.SupervisorNamespace, env.SupervisorUpstreamLDAP.Host, + indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName)))) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`ldapidentityprovider.idp.supervisor.%s`, env.APIGroupSuffix), + tc.wantErrorSnippets, + "LDAPIdentityProvider", + resourceName, + ) + + if tc.wantErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.SupervisorNamespace, + "LDAPIdentityProvider", + tc.wantTLSValidConditionMessage(env.SupervisorNamespace, secretOrConfigmapResourceName), + ) + } + }) + + t.Run("apply ActiveDirectory IDP", func(t *testing.T) { + resourceName := "test-ad-idp-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.SupervisorNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + yamlBytes := []byte(fmt.Sprintf(activeDirectoryIDPTemplate, + env.APIGroupSuffix, resourceName, env.SupervisorNamespace, env.SupervisorUpstreamLDAP.Host, + indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName)))) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`activedirectoryidentityprovider.idp.supervisor.%s`, env.APIGroupSuffix), + tc.wantErrorSnippets, + "ActiveDirectoryIdentityProvider", + resourceName, + ) + + if tc.wantErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.SupervisorNamespace, + "ActiveDirectoryIdentityProvider", + tc.wantTLSValidConditionMessage(env.SupervisorNamespace, secretOrConfigmapResourceName), + ) + } + }) + + t.Run("apply GitHub IDP", func(t *testing.T) { + resourceName := "test-github-idp-" + testlib.RandHex(t, 7) + + secretOrConfigmapResourceName := createSecretOrConfigMapFromData(t, + resourceName, + env.SupervisorNamespace, + tc.secretOrConfigmapKind, + tc.secretType, + tc.secretOrConfigmapDataYAML, + ) + + // GitHub is nested deeper. + indentedTLSYAMLForGitHub := indentForHeredoc(indentForHeredoc(tc.tlsYAML(secretOrConfigmapResourceName))) + + yamlBytes := []byte(fmt.Sprintf(githubIDPTemplate, + env.APIGroupSuffix, resourceName, env.SupervisorNamespace, indentedTLSYAMLForGitHub)) + + stdOut, stdErr, err := performKubectlApply(t, resourceName, yamlBytes) + requireKubectlApplyResult(t, stdOut, stdErr, err, + fmt.Sprintf(`githubidentityprovider.idp.supervisor.%s`, env.APIGroupSuffix), + tc.wantGitHubErrorSnippets, + "GitHubIdentityProvider", + resourceName, + ) + + if tc.wantGitHubErrorSnippets == nil { + requireTLSValidConditionMessageOnResource(t, + resourceName, + env.SupervisorNamespace, + "GitHubIdentityProvider", + // The tls spec location is different for GitHubIdentityProvider, so adjust the expectation. + strings.Replace( + tc.wantTLSValidConditionMessage(env.SupervisorNamespace, secretOrConfigmapResourceName), + "spec.tls", "spec.githubAPI.tls", 1), + ) + } + }) + }) + } +} diff --git a/test/integration/supervisor_upstream_test.go b/test/integration/supervisor_upstream_test.go index 0c7e463d7..c20da7301 100644 --- a/test/integration/supervisor_upstream_test.go +++ b/test/integration/supervisor_upstream_test.go @@ -46,6 +46,7 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", }, + expectedTLSConfigValidCondition(false), // we are not configuring a CA bundle on the OIDCIdentityProvider in this test }) }) @@ -84,6 +85,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", }, + expectedTLSConfigValidCondition(env.SupervisorUpstreamOIDC.CABundle != ""), }) }) @@ -121,6 +123,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", }, + expectedTLSConfigValidCondition(env.SupervisorUpstreamOIDC.CABundle != ""), }) }) } @@ -135,3 +138,18 @@ func expectUpstreamConditions(t *testing.T, upstream *idpv1alpha1.OIDCIdentityPr } require.ElementsMatch(t, expected, normalized) } + +func expectedTLSConfigValidCondition(caBundleConfigured bool) metav1.Condition { + c := metav1.Condition{ + Type: "TLSConfigurationValid", + Status: "True", + Reason: "Success", + Message: "spec.tls is valid: no TLS configuration provided: using default root CA bundle from container image", + } + + if caBundleConfigured { + c.Message = "spec.tls is valid: using configured CA bundle" + } + + return c +} diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index 17da03367..f633d7fb3 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -7,7 +7,6 @@ import ( "encoding/base64" "fmt" "io" - "net/url" "os" "os/exec" "path/filepath" @@ -49,12 +48,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { pinnipedExe := testlib.PinnipedCLIPath(t) tempDir := t.TempDir() - // Infer the downstream issuer URL from the callback associated with the upstream test client registration. - issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) - require.NoError(t, err) - require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) - issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") - t.Logf("testing with downstream issuer URL %s", issuerURL.String()) + issuerURL, _ := env.InferSupervisorIssuerURL(t) // Generate a CA bundle with which to serve this provider. t.Logf("generating test CA") diff --git a/test/testlib/client.go b/test/testlib/client.go index 504ef0bdf..36f455e82 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -453,6 +453,27 @@ func RandHex(t *testing.T, numBytes int) string { return hex.EncodeToString(RandBytes(t, numBytes)) } +func CreateTestConfigMap(t *testing.T, namespace string, baseName string, stringData map[string]string) *corev1.ConfigMap { + t.Helper() + client := NewKubernetesClientset(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + created, err := client.CoreV1().ConfigMaps(namespace).Create(ctx, &corev1.ConfigMap{ + ObjectMeta: TestObjectMeta(t, baseName), + Data: stringData, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + t.Logf("cleaning up test Configmap %s/%s", created.Namespace, created.Name) + err := client.CoreV1().ConfigMaps(namespace).Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + t.Logf("created test ConfigMap %s/%s", created.Namespace, created.Name) + return created +} + func CreateTestSecret(t *testing.T, namespace string, baseName string, secretType corev1.SecretType, stringData map[string]string) *corev1.Secret { t.Helper() client := NewKubernetesClientset(t) @@ -471,7 +492,7 @@ func CreateTestSecret(t *testing.T, namespace string, baseName string, secretTyp err := client.CoreV1().Secrets(namespace).Delete(context.Background(), created.Name, metav1.DeleteOptions{}) require.NoError(t, err) }) - t.Logf("created test Secret %s", created.Name) + t.Logf("created test Secret %s/%s", created.Namespace, created.Name) return created } diff --git a/test/testlib/env.go b/test/testlib/env.go index 5e83caa43..23833c783 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -5,6 +5,7 @@ package testlib import ( "encoding/base64" + "net/url" "os" "sort" "strings" @@ -83,6 +84,20 @@ type TestOIDCUpstream struct { ExpectedGroups []string `json:"expectedGroups"` } +// InferSupervisorIssuerURL infers the downstream issuer URL from the callback associated with the upstream test client registration. +func (e *TestEnv) InferSupervisorIssuerURL(t *testing.T) (*url.URL, string) { + t.Helper() + issuerURL, err := url.Parse(e.SupervisorUpstreamOIDC.CallbackURL) + require.NoError(t, err) + require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) + issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") + + issuerAsString := issuerURL.String() + t.Logf("testing with downstream issuer URL %s", issuerAsString) + + return issuerURL, issuerAsString +} + type TestLDAPUpstream struct { Host string `json:"host"` Domain string `json:"domain"`