From be11966a647cd46ce1a6711f0a24e6b8e351dd4c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 15 Feb 2023 12:08:41 -0800 Subject: [PATCH 01/81] Add APIs for multiple IDP and id transformations to FederationDomain CRD --- .../v1alpha1/types_federationdomain.go.tmpl | 181 +++++++++++- ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.21/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.22/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.23/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.24/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.25/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- generated/1.26/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ 28 files changed, 5047 insertions(+), 120 deletions(-) diff --git a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl index 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index 71f7370d1..396d25511 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index d34a634ed..9a5fcd685 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -652,6 +652,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -689,7 +708,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -714,7 +736,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -746,8 +768,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -781,7 +903,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -806,9 +928,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -833,7 +955,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -845,7 +967,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1165,7 +1287,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1333,7 +1455,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1498,7 +1620,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go index 27de4401c..2048b4e77 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 71f7370d1..396d25511 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 8d17cd497..58e9b9353 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -652,6 +652,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -689,7 +708,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -714,7 +736,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -746,8 +768,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -781,7 +903,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -806,9 +928,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -833,7 +955,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -845,7 +967,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1165,7 +1287,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1333,7 +1455,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1498,7 +1620,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go index 27de4401c..2048b4e77 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 71f7370d1..396d25511 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index c6117ae21..dfd2ef936 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -652,6 +652,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -689,7 +708,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -714,7 +736,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -746,8 +768,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -781,7 +903,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -806,9 +928,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -833,7 +955,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -845,7 +967,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1165,7 +1287,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1333,7 +1455,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1498,7 +1620,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go index 27de4401c..2048b4e77 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 71f7370d1..396d25511 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index b53ace67c..c68bd1371 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -652,6 +652,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -689,7 +708,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -714,7 +736,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -746,8 +768,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -781,7 +903,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -806,9 +928,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -833,7 +955,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -845,7 +967,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1165,7 +1287,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1333,7 +1455,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1498,7 +1620,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in 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 71f7370d1..396d25511 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 04d0ab663..30e9a6860 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -650,6 +650,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -687,7 +706,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -712,7 +734,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -744,8 +766,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -779,7 +901,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -804,9 +926,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -831,7 +953,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -843,7 +965,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1161,7 +1283,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1329,7 +1451,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1494,7 +1616,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in 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 71f7370d1..396d25511 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index 69bf458b7..a8aa6a916 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -650,6 +650,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -687,7 +706,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -712,7 +734,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -744,8 +766,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -779,7 +901,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -804,9 +926,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -831,7 +953,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -843,7 +965,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1161,7 +1283,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1329,7 +1451,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1494,7 +1616,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in 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 71f7370d1..396d25511 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go index 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 551886ceb..539158fac 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -40,6 +40,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -101,6 +119,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -151,6 +176,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in From 5385fb38dbf77d47a3f9b5fc5179d3d9870cb8c4 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 6 Feb 2023 14:43:50 -0800 Subject: [PATCH 02/81] Add identity transformation packages idtransform and celformer Implements Supervisor identity transformations helpers using CEL. --- go.mod | 2 +- internal/celtransformer/celformer.go | 278 +++++++ internal/celtransformer/celformer_test.go | 734 ++++++++++++++++++ .../idtransform/identity_transformations.go | 72 ++ 4 files changed, 1085 insertions(+), 1 deletion(-) create mode 100644 internal/celtransformer/celformer.go create mode 100644 internal/celtransformer/celformer_test.go create mode 100644 internal/idtransform/identity_transformations.go diff --git a/go.mod b/go.mod index 83e99e9e1..2f8185f76 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/go-logr/zapr v1.2.4 github.com/gofrs/flock v0.8.1 github.com/golang/mock v1.6.0 + github.com/google/cel-go v0.16.0 github.com/google/go-cmp v0.5.9 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.3.1 @@ -90,7 +91,6 @@ require ( github.com/golang/glog v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/cel-go v0.16.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go new file mode 100644 index 000000000..409e064b8 --- /dev/null +++ b/internal/celtransformer/celformer.go @@ -0,0 +1,278 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package celtransformer is an implementation of upstream-to-downstream identity transformations +// and policies using CEL scripts. +// +// The CEL language is documented in https://github.com/google/cel-spec/blob/master/doc/langdef.md +// with optional extensions documented in https://github.com/google/cel-go/tree/master/ext. +package celtransformer + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/ext" + + "go.pinniped.dev/internal/idtransform" +) + +const ( + usernameVariableName = "username" + groupsVariableName = "groups" + + defaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy" +) + +// CELTransformer can compile any number of transformation expression pipelines. +// Each compiled pipeline can be cached in memory for later thread-safe evaluation. +type CELTransformer struct { + compiler *cel.Env + maxExpressionRuntime time.Duration +} + +// NewCELTransformer returns a CELTransformer. +// A running process should only need one instance of a CELTransformer. +func NewCELTransformer(maxExpressionRuntime time.Duration) (*CELTransformer, error) { + env, err := newEnv() + if err != nil { + return nil, err + } + return &CELTransformer{compiler: env, maxExpressionRuntime: maxExpressionRuntime}, nil +} + +// CompileTransformation compiles a CEL-based identity transformation expression. +// The compiled transform can be cached in memory and executed repeatedly and in a thread-safe way. +func (c *CELTransformer) CompileTransformation(t CELTransformation) (idtransform.IdentityTransformation, error) { + return t.compile(c) +} + +// CELTransformation can be compiled into an IdentityTransformation. +type CELTransformation interface { + compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) +} + +// UsernameTransformation is a CEL expression that can transform a username (or leave it unchanged). +// It implements CELTransformation. +type UsernameTransformation struct { + Expression string +} + +// GroupsTransformation is a CEL expression that can transform a list of group names (or leave it unchanged). +// It implements CELTransformation. +type GroupsTransformation struct { + Expression string +} + +// AllowAuthenticationPolicy is a CEL expression that can allow the authentication to proceed by returning true. +// It implements CELTransformation. When the CEL expression returns false, the authentication is rejected and the +// RejectedAuthenticationMessage is used. When RejectedAuthenticationMessage is empty, a default message will be +// used for rejected authentications. +type AllowAuthenticationPolicy struct { + Expression string + RejectedAuthenticationMessage string +} + +func compileProgram(transformer *CELTransformer, expectedExpressionType *cel.Type, expr string) (cel.Program, error) { + if strings.TrimSpace(expr) == "" { + return nil, fmt.Errorf("cannot compile empty CEL expression") + } + + // compile does both parsing and type checking. The parsing phase indicates whether the expression is + // syntactically valid and expands any macros present within the environment. Parsing and checking are + // more computationally expensive than evaluation, so parsing and checking are done in advance. + ast, issues := transformer.compiler.Compile(expr) + if issues != nil { + return nil, fmt.Errorf("CEL expression compile error: %s", issues.String()) + } + + // The compiler's type checker has determined the type of the expression's result. + // Check that it matches the type that we expect. + if ast.OutputType().String() != expectedExpressionType.String() { + return nil, fmt.Errorf("CEL expression should return type %q but returns type %q", expectedExpressionType, ast.OutputType()) + } + + // The cel.Program is stateless, thread-safe, and cachable. + program, err := transformer.compiler.Program(ast, + cel.InterruptCheckFrequency(100), // Kubernetes uses 100 here, so we'll copy that setting. + cel.EvalOptions(cel.OptOptimize), // Optimize certain things now rather than at evaluation time. + ) + if err != nil { + return nil, fmt.Errorf("CEL expression program construction error: %w", err) + } + return program, nil +} + +func (t *UsernameTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { + program, err := compileProgram(transformer, cel.StringType, t.Expression) + if err != nil { + return nil, err + } + return &compiledUsernameTransformation{ + program: program, + maxExpressionRuntime: transformer.maxExpressionRuntime, + }, nil +} + +func (t *GroupsTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { + program, err := compileProgram(transformer, cel.ListType(cel.StringType), t.Expression) + if err != nil { + return nil, err + } + return &compiledGroupsTransformation{ + program: program, + maxExpressionRuntime: transformer.maxExpressionRuntime, + }, nil +} + +func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { + program, err := compileProgram(transformer, cel.BoolType, t.Expression) + if err != nil { + return nil, err + } + return &compiledAllowAuthenticationPolicy{ + program: program, + maxExpressionRuntime: transformer.maxExpressionRuntime, + rejectedAuthenticationMessage: t.RejectedAuthenticationMessage, + }, nil +} + +// Implements idtransform.IdentityTransformation. +type compiledUsernameTransformation struct { + program cel.Program + maxExpressionRuntime time.Duration +} + +// Implements idtransform.IdentityTransformation. +type compiledGroupsTransformation struct { + program cel.Program + maxExpressionRuntime time.Duration +} + +// Implements idtransform.IdentityTransformation. +type compiledAllowAuthenticationPolicy struct { + program cel.Program + maxExpressionRuntime time.Duration + rejectedAuthenticationMessage string +} + +func evalProgram(ctx context.Context, program cel.Program, maxExpressionRuntime time.Duration, username string, groups []string) (ref.Val, error) { + // Limit the runtime of a CEL expression to avoid accidental very expensive expressions. + timeoutCtx, cancel := context.WithTimeout(ctx, maxExpressionRuntime) + defer cancel() + + // Evaluation is thread-safe and side effect free. Many inputs can be sent to the same cel.Program + // and if fields are present in the input, but not referenced in the expression, they are ignored. + // The argument to Eval may either be an `interpreter.Activation` or a `map[string]interface{}`. + val, _, err := program.ContextEval(timeoutCtx, map[string]interface{}{ + usernameVariableName: username, + groupsVariableName: groups, + }) + return val, err +} + +func (c *compiledUsernameTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { + val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + if err != nil { + return nil, err + } + nativeValue, err := val.ConvertToNative(reflect.TypeOf("")) + if err != nil { + return nil, fmt.Errorf("could not convert expression result to string: %w", err) + } + stringValue, ok := nativeValue.(string) + if !ok { + return nil, fmt.Errorf("could not convert expression result to string") + } + return &idtransform.TransformationResult{ + Username: stringValue, + Groups: groups, // groups are not modified by username transformations + AuthenticationAllowed: true, + }, nil +} + +func (c *compiledGroupsTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { + val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + if err != nil { + return nil, err + } + nativeValue, err := val.ConvertToNative(reflect.TypeOf([]string{})) + if err != nil { + return nil, fmt.Errorf("could not convert expression result to []string: %w", err) + } + stringSliceValue, ok := nativeValue.([]string) + if !ok { + return nil, fmt.Errorf("could not convert expression result to []string") + } + return &idtransform.TransformationResult{ + Username: username, // username is not modified by groups transformations + Groups: stringSliceValue, + AuthenticationAllowed: true, + }, nil +} + +func (c *compiledAllowAuthenticationPolicy) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { + val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + if err != nil { + return nil, err + } + nativeValue, err := val.ConvertToNative(reflect.TypeOf(true)) + if err != nil { + return nil, fmt.Errorf("could not convert expression result to bool: %w", err) + } + boolValue, ok := nativeValue.(bool) + if !ok { + return nil, fmt.Errorf("could not convert expression result to bool") + } + result := &idtransform.TransformationResult{ + Username: username, // username is not modified by policies + Groups: groups, // groups are not modified by policies + AuthenticationAllowed: boolValue, + } + if !boolValue { + if len(c.rejectedAuthenticationMessage) == 0 { + result.RejectedAuthenticationMessage = defaultPolicyRejectedAuthMessage + } else { + result.RejectedAuthenticationMessage = c.rejectedAuthenticationMessage + } + } + return result, nil +} + +func newEnv() (*cel.Env, error) { + // Note that Kubernetes uses CEL in several places, which are helpful to see as an example of + // how to configure the CEL compiler for production usage. Examples: + // https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler.go + // https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go + return cel.NewEnv( + // Declare our variable without giving them values yet. By declaring them here, the type is known during + // the parsing/checking phase. + cel.Variable(usernameVariableName, cel.StringType), + cel.Variable(groupsVariableName, cel.ListType(cel.StringType)), + + // Enable the strings extensions. + // See https://github.com/google/cel-go/tree/master/ext#strings + // CEL also has other extensions for bas64 encoding/decoding and for math that we could choose to enable. + // See https://github.com/google/cel-go/tree/master/ext + // Kubernetes adds more extensions for extra regexp helpers, URLs, and extra list helpers that we could also + // consider enabling. Note that if we added their regexp extension, then we would also need to add + // cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...) as an option when we call cel.Program. + // See https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/apiserver/pkg/cel/library + ext.Strings(), + + // Just in case someone converts a string to a timestamp, make any time operations which do not include + // an explicit timezone argument default to UTC. + cel.DefaultUTCTimeZone(true), + + // Check list and map literal entry types during type-checking. + cel.HomogeneousAggregateLiterals(), + + // Check for collisions in declarations now instead of later. + cel.EagerlyValidateDeclarations(true), + ) +} diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go new file mode 100644 index 000000000..762f8ecf3 --- /dev/null +++ b/internal/celtransformer/celformer_test.go @@ -0,0 +1,734 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package celtransformer + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/idtransform" +) + +func TestTransformer(t *testing.T) { + var veryLargeGroupList []string + for i := 0; i < 10000; i++ { + veryLargeGroupList = append(veryLargeGroupList, fmt.Sprintf("g%d", i)) + } + + alreadyCancelledContext, cancel := context.WithCancel(context.Background()) + cancel() + + tests := []struct { + name string + username string + groups []string + transforms []CELTransformation + ctx context.Context + + wantUsername string + wantGroups []string + wantAuthRejected bool + wantAuthRejectedMessage string + wantCompileErr string + wantEvaluationErr string + }{ + { + name: "empty transforms list does not change the identity and allows auth", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{}, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "simple transforms which do not change the identity and allows auth", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username`}, + &GroupsTransformation{Expression: `groups`}, + &AllowAuthenticationPolicy{Expression: `true`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "transformations run in the order that they are given and accumulate results", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `"a:" + username`}, + &UsernameTransformation{Expression: `"b:" + username`}, + &GroupsTransformation{Expression: `groups.map(g, "a:" + g)`}, + &GroupsTransformation{Expression: `groups.map(g, "b:" + g)`}, + }, + wantUsername: "b:a:ryan", + wantGroups: []string{"b:a:admins", "b:a:developers", "b:a:other"}, + }, + { + name: "policies which return false cause the pipeline to stop running and return a rejected auth result", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `"a:" + username`}, + &AllowAuthenticationPolicy{Expression: `true`, RejectedAuthenticationMessage: `Everyone is allowed`}, + &GroupsTransformation{Expression: `groups.map(g, "a:" + g)`}, + &AllowAuthenticationPolicy{Expression: `username == "admin"`, RejectedAuthenticationMessage: `Only the username "admin" is allowed`}, + &GroupsTransformation{Expression: `groups.map(g, "b:" + g)`}, // does not get evaluated + }, + wantUsername: "a:ryan", + wantGroups: []string{"a:admins", "a:developers", "a:other"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only the username "admin" is allowed`, + }, + { + name: "policies without a RejectedAuthenticationMessage get a default message", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `username == "admin"`, RejectedAuthenticationMessage: ""}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Authentication was rejected by a configured policy`, + }, + { + name: "any transformations can use the username and group variables", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `groups[0] == "admins" && username == "ryan"`}, + &GroupsTransformation{Expression: `groups + [username]`}, + &UsernameTransformation{Expression: `groups[2]`}, // changes the username to "other" + &GroupsTransformation{Expression: `groups + [username + "2"]`}, // by the time this expression runs, the username was already changed to "other" + }, + wantUsername: "other", + wantGroups: []string{"admins", "developers", "other", "ryan", "other2"}, + }, + { + name: "the CEL string extensions are enabled for use in the expressions", + username: " ryan ", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.map(g, g.replace("mins", "ministrators"))`}, + &UsernameTransformation{Expression: `username.upperAscii()`}, + &AllowAuthenticationPolicy{Expression: `(username.lowerAscii()).trim() == "ryan"`, RejectedAuthenticationMessage: `Silly example`}, + &UsernameTransformation{Expression: `username.trim()`}, + }, + wantUsername: "RYAN", + wantGroups: []string{"administrators", "developers", "other"}, + }, + { + name: "UTC is the default time zone for time operations", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `string(timestamp("2023-01-16T10:00:20.021-08:00").getHours())`}, + }, + // Without the compiler option cel.DefaultUTCTimeZone, this result would be 10. + // With the option, this result is the original hour from the timestamp string (10), plus the effect + // of the timezone (8), to move the hour into the UTC time zone. + wantUsername: "18", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "the default UTC time zone for time operations can be overridden by passing the time zone as an argument to the operation", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `string(timestamp("2023-01-16T10:00:20.021-08:00").getHours("US/Mountain"))`}, + }, + // This is the hour of the timestamp in Mountain time, which is one time zone over from Pacific (-08:00), + // hence it is one larger than the original "10" from the timestamp string. + wantUsername: "11", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "quick expressions are finished by CEL before CEL even looks at the cancel context", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `["one group"]`}, + }, + ctx: alreadyCancelledContext, + wantUsername: "ryan", + wantGroups: []string{"one group"}, + }, + + // + // Unit tests to demonstrate practical examples of useful CEL expressions. + // + { + name: "can prefix username and all groups", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `"username_prefix:" + username`}, + &GroupsTransformation{Expression: `groups.map(g, "group_prefix:" + g)`}, + }, + wantUsername: "username_prefix:ryan", + wantGroups: []string{"group_prefix:admins", "group_prefix:developers", "group_prefix:other"}, + }, + { + name: "can suffix username and all groups", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username + ":username_suffix"`}, + &GroupsTransformation{Expression: `groups.map(g, g + ":group_suffix")`}, + }, + wantUsername: "ryan:username_suffix", + wantGroups: []string{"admins:group_suffix", "developers:group_suffix", "other:group_suffix"}, + }, + { + name: "can change case of username and all groups", + username: "rYan 🚀", + groups: []string{"aDmins", "dEvelopers", "oTher"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username.lowerAscii()`}, + &GroupsTransformation{Expression: `groups.map(g, g.upperAscii())`}, + }, + wantUsername: "ryan 🚀", + wantGroups: []string{"ADMINS", "DEVELOPERS", "OTHER"}, + }, + { + name: "can replace whitespace", + username: " r\ty a n \n", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username.replace(" ", "").replace("\n", "").replace("\t", "")`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "can filter groups based on an allow list", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(g, g in ["admins", "developers"])`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers"}, + }, + { + name: "can filter groups based on a disallow list", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(g, !(g in ["admins", "developers"]))`}, + }, + wantUsername: "ryan", + wantGroups: []string{"other"}, + }, + { + name: "can filter groups based on a disallowed prefixes", + username: "ryan", + groups: []string{"disallowed1:admins", "disallowed2:developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(group, !(["disallowed1:", "disallowed2:"].exists(prefix, group.startsWith(prefix))))`}, + }, + wantUsername: "ryan", + wantGroups: []string{"other"}, + }, + { + name: "can add a group", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups + ["new-group"]`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "new-group"}, + }, + { + name: "can add a group but only if they already belong to another group - when the user does belong to that other group", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `"other" in groups ? groups + ["new-group"] : groups`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "new-group"}, + }, + { + name: "can add a group but only if they already belong to another group - when the user does NOT belong to that other group", + username: "ryan", + groups: []string{"admins", "developers"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `"other" in groups ? groups + ["new-group"] : groups`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers"}, + }, + { + name: "can rename a group", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.map(g, g == "other" ? "other-renamed" : g)`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other-renamed"}, + }, + { + name: "can reject auth based on belonging to one group - when the user meets the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other", "super-admins"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `"super-admins" in groups`, RejectedAuthenticationMessage: `Only users who belong to the "super-admins" group are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "super-admins"}, + }, + { + name: "can reject auth based on belonging to one group - when the user does NOT meet the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `"super-admins" in groups`, RejectedAuthenticationMessage: `Only users who belong to the "super-admins" group are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only users who belong to the "super-admins" group are allowed`, + }, + { + name: "can reject auth unless the user belongs to any one of the groups in a list - when the user meets the criteria", + username: "ryan", + groups: []string{"admins", "developers", "foobar", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `groups.exists(g, g in ["foobar", "foobaz", "foobat"])`, RejectedAuthenticationMessage: `Only users who belong to any of the groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "foobar", "other"}, + }, + { + name: "can reject auth unless the user belongs to any one of the groups in a list - when the user does NOT meet the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `groups.exists(g, g in ["foobar", "foobaz", "foobat"])`, RejectedAuthenticationMessage: `Only users who belong to any of the groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only users who belong to any of the groups in a list are allowed`, + }, + { + name: "can reject auth unless the user belongs to all of the groups in a list - when the user meets the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other", "foobar", "foobaz", "foobat"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `["foobar", "foobaz", "foobat"].all(g, g in groups)`, RejectedAuthenticationMessage: `Only users who belong to all groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "foobar", "foobaz", "foobat"}, + }, + { + name: "can reject auth unless the user belongs to all of the groups in a list - when the user does NOT meet the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other", "foobaz", "foobat"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `["foobar", "foobaz", "foobat"].all(g, g in groups)`, RejectedAuthenticationMessage: `Only users who belong to all groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "foobaz", "foobat"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only users who belong to all groups in a list are allowed`, + }, + { + name: "can reject auth if the user belongs to any groups in a disallowed groups list - when the user meets the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `!groups.exists(g, g in ["foobar", "foobaz"])`, RejectedAuthenticationMessage: `Only users who do not belong to any of the groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "can reject auth if the user belongs to any groups in a disallowed groups list - when the user does NOT meet the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other", "foobaz"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `!groups.exists(g, g in ["foobar", "foobaz"])`, RejectedAuthenticationMessage: `Only users who do not belong to any of the groups in a list are allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "foobaz"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only users who do not belong to any of the groups in a list are allowed`, + }, + { + name: "can reject auth unless the username is in an allowed users list - when the user meets the criteria", + username: "foobaz", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `username in ["foobar", "foobaz"]`, RejectedAuthenticationMessage: `Only certain usernames allowed`}, + }, + wantUsername: "foobaz", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "can reject auth unless the username is in an allowed users list - when the user does NOT meet the criteria", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `username in ["foobar", "foobaz"]`, RejectedAuthenticationMessage: `Only certain usernames allowed`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other"}, + wantAuthRejected: true, + wantAuthRejectedMessage: `Only certain usernames allowed`, + }, + + // + // Error cases + // + { + name: "username transformation returns an empty string as the new username", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `""`}, + }, + wantEvaluationErr: "identity transformation returned an empty username, which is not allowed", + }, + { + name: "username transformation returns a string containing only whitespace as the new username", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `" \n \t "`}, + }, + wantEvaluationErr: "identity transformation returned an empty username, which is not allowed", + }, + { + name: "username transformation compiles to return null, which is not a string so it has the wrong type", + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `null`}, + }, + wantCompileErr: `CEL expression should return type "string" but returns type "null_type"`, + }, + { + name: "groups transformation compiles to return null, which is not a string so it has the wrong type", + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `null`}, + }, + wantCompileErr: `CEL expression should return type "list(string)" but returns type "null_type"`, + }, + { + name: "policy transformation compiles to return null, which is not a string so it has the wrong type", + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `null`}, + }, + wantCompileErr: `CEL expression should return type "bool" but returns type "null_type"`, + }, + { + name: "username transformation has empty expression", + transforms: []CELTransformation{ + &UsernameTransformation{Expression: ``}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "groups transformation has empty expression", + transforms: []CELTransformation{ + &GroupsTransformation{Expression: ``}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "policy transformation has empty expression", + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: ``}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "username transformation has expression which contains only whitespace", + transforms: []CELTransformation{ + &UsernameTransformation{Expression: " \n\t "}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "groups transformation has expression which contains only whitespace", + transforms: []CELTransformation{ + &GroupsTransformation{Expression: " \n\t "}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "policy transformation has expression which contains only whitespace", + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: " \n\t "}, + }, + wantCompileErr: `cannot compile empty CEL expression`, + }, + { + name: "slow username transformation expressions are canceled by the cancel context after partial evaluation", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, + }, + ctx: alreadyCancelledContext, + wantEvaluationErr: `identity transformation at index 0: operation interrupted`, + }, + { + name: "slow groups transformation expressions are canceled by the cancel context after partial evaluation", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(x, groups.all(x, true))`}, + }, + ctx: alreadyCancelledContext, + wantEvaluationErr: `identity transformation at index 0: operation interrupted`, + }, + { + name: "slow policy expressions are canceled by the cancel context after partial evaluation", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: "username"}, + &AllowAuthenticationPolicy{Expression: `groups.all(x, groups.all(x, true))`}, // this is the slow one + }, + ctx: alreadyCancelledContext, + wantEvaluationErr: `identity transformation at index 1: operation interrupted`, + }, + { + name: "slow transformation expressions are canceled and the rest of the expressions do not run", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username`}, // quick expressions are allowed to run even though the context is cancelled + &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, + &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, + &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, + }, + ctx: alreadyCancelledContext, + wantEvaluationErr: `identity transformation at index 1: operation interrupted`, + }, + { + name: "slow username transformation expressions are canceled after a maximum allowed duration", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. + &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, + }, + wantEvaluationErr: `identity transformation at index 0: operation interrupted`, + }, + { + name: "slow groups transformation expressions are canceled after a maximum allowed duration", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. + &GroupsTransformation{Expression: `groups.filter(x, groups.all(x, true))`}, + }, + wantEvaluationErr: `identity transformation at index 0: operation interrupted`, + }, + { + name: "slow policy transformation expressions are canceled after a maximum allowed duration", + username: "ryan", + groups: veryLargeGroupList, + transforms: []CELTransformation{ + // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. + &AllowAuthenticationPolicy{Expression: `groups.all(x, groups.all(x, true))`}, + }, + wantEvaluationErr: `identity transformation at index 0: operation interrupted`, + }, + { + name: "compile errors are returned by the compile step for a username transform", + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `foobar.junk()`}, + }, + wantCompileErr: here.Doc(` + CEL expression compile error: ERROR: :1:1: undeclared reference to 'foobar' (in container '') + | foobar.junk() + | ^ + ERROR: :1:12: undeclared reference to 'junk' (in container '') + | foobar.junk() + | ...........^`, + ), + }, + { + name: "compile errors are returned by the compile step for a groups transform", + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `foobar.junk()`}, + }, + wantCompileErr: here.Doc(` + CEL expression compile error: ERROR: :1:1: undeclared reference to 'foobar' (in container '') + | foobar.junk() + | ^ + ERROR: :1:12: undeclared reference to 'junk' (in container '') + | foobar.junk() + | ...........^`, + ), + }, + { + name: "compile errors are returned by the compile step for a policy", + transforms: []CELTransformation{ + &AllowAuthenticationPolicy{Expression: `foobar.junk()`}, + }, + wantCompileErr: here.Doc(` + CEL expression compile error: ERROR: :1:1: undeclared reference to 'foobar' (in container '') + | foobar.junk() + | ^ + ERROR: :1:12: undeclared reference to 'junk' (in container '') + | foobar.junk() + | ...........^`, + ), + }, + { + name: "evaluation errors stop the pipeline and return an error", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: "username"}, + &AllowAuthenticationPolicy{Expression: `1 / 0 == 7`}, + }, + wantEvaluationErr: `identity transformation at index 1: division by zero`, + }, + { + name: "HomogeneousAggregateLiterals compiler setting is enabled to help the user avoid type mistakes in expressions", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.all(g, g in ["admins", 1])`}, + }, + wantCompileErr: here.Doc(` + CEL expression compile error: ERROR: :1:31: expected type 'string' but found 'int' + | groups.all(g, g in ["admins", 1]) + | ..............................^`, + ), + }, + { + name: "when an expression's type cannot be determined at compile time, e.g. due to the use of dynamic types", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.map(g, {"admins": dyn(1), "developers":"a"}[g])`}, + }, + wantCompileErr: `CEL expression should return type "list(string)" but returns type "list(dyn)"`, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + transformer, err := NewCELTransformer(100 * time.Millisecond) + require.NoError(t, err) + + pipeline := idtransform.NewTransformationPipeline() + + for _, transform := range tt.transforms { + compiledTransform, err := transformer.CompileTransformation(transform) + if tt.wantCompileErr != "" { + require.EqualError(t, err, tt.wantCompileErr) + return // the rest of the test doesn't make sense when there was a compile error + } + require.NoError(t, err, "got an unexpected compile error") + pipeline.AppendTransformation(compiledTransform) + } + + ctx := context.Background() + if tt.ctx != nil { + ctx = tt.ctx + } + + result, err := pipeline.Evaluate(ctx, tt.username, tt.groups) + if tt.wantEvaluationErr != "" { + require.EqualError(t, err, tt.wantEvaluationErr) + return // the rest of the test doesn't make sense when there was an evaluation error + } + require.NoError(t, err, "got an unexpected evaluation error") + + require.Equal(t, tt.wantUsername, result.Username) + require.Equal(t, tt.wantGroups, result.Groups) + require.Equal(t, !tt.wantAuthRejected, result.AuthenticationAllowed, "AuthenticationAllowed had unexpected value") + require.Equal(t, tt.wantAuthRejectedMessage, result.RejectedAuthenticationMessage) + }) + } +} + +func TestTypicalPerformanceAndThreadSafety(t *testing.T) { + t.Parallel() + + transformer, err := NewCELTransformer(100 * time.Millisecond) + require.NoError(t, err) + + pipeline := idtransform.NewTransformationPipeline() + + var compiledTransform idtransform.IdentityTransformation + compiledTransform, err = transformer.CompileTransformation(&UsernameTransformation{Expression: `"username_prefix:" + username`}) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform) + compiledTransform, err = transformer.CompileTransformation(&GroupsTransformation{Expression: `groups.map(g, "group_prefix:" + g)`}) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform) + compiledTransform, err = transformer.CompileTransformation(&AllowAuthenticationPolicy{Expression: `username == "username_prefix:ryan"`}) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform) + + var groups []string + var wantGroups []string + for i := 0; i < 100; i++ { + groups = append(groups, fmt.Sprintf("g%d", i)) + wantGroups = append(wantGroups, fmt.Sprintf("group_prefix:g%d", i)) + } + + // Before looking at performance, check that the behavior of the function is correct. + result, err := pipeline.Evaluate(context.Background(), "ryan", groups) + require.NoError(t, err) + require.Equal(t, "username_prefix:ryan", result.Username) + require.Equal(t, wantGroups, result.Groups) + require.True(t, result.AuthenticationAllowed) + require.Empty(t, result.RejectedAuthenticationMessage) + + // This loop is meant to give a sense of typical runtime of CEL expressions which transforms a username + // and 100 group names. It is not meant to be a pass/fail test or scientific benchmark test. + iterations := 1000 + start := time.Now() + for i := 0; i < iterations; i++ { + _, _ = pipeline.Evaluate(context.Background(), "ryan", groups) + } + elapsed := time.Since(start) + t.Logf("TestTypicalPerformanceAndThreadSafety %d iterations of Evaluate took %s; average runtime %s", iterations, elapsed, elapsed/time.Duration(iterations)) + // On my laptop this prints: TestTypicalPerformanceAndThreadSafety 1000 iterations of Evaluate took 257.981421ms; average runtime 257.981µs + + // Now use the transformations pipeline from different goroutines at the same time. Hopefully the race detector + // will complain if this is not thread safe in some way. Use the pipeline enough that it will be very likely that + // there will be several parallel invocations of the Evaluate function. Every invocation should also yield the + // exact same result, since they are all using the same inputs. This assumes that the unit tests are run using + // the race detector. + var wg sync.WaitGroup + numGoroutines := 10 + for i := 0; i < numGoroutines; i++ { + wg.Add(1) // increment WaitGroup counter for each goroutine + go func() { + defer wg.Done() // decrement WaitGroup counter when this goroutine finishes + for j := 0; j < iterations*2; j++ { + localResult, localErr := pipeline.Evaluate(context.Background(), "ryan", groups) + require.NoError(t, localErr) + require.Equal(t, "username_prefix:ryan", localResult.Username) + require.Equal(t, wantGroups, localResult.Groups) + require.True(t, localResult.AuthenticationAllowed) + require.Empty(t, localResult.RejectedAuthenticationMessage) + } + }() + } + wg.Wait() // wait for the counter to reach zero, indicating the all goroutines are finished +} diff --git a/internal/idtransform/identity_transformations.go b/internal/idtransform/identity_transformations.go new file mode 100644 index 000000000..d7fb4d31f --- /dev/null +++ b/internal/idtransform/identity_transformations.go @@ -0,0 +1,72 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package idtransform defines upstream-to-downstream identity transformations which could be +// implemented using various approaches or languages. +package idtransform + +import ( + "context" + "fmt" + "strings" +) + +// TransformationResult is the result of evaluating a transformation against some inputs. +type TransformationResult struct { + Username string // the new username for an allowed auth + Groups []string // the new group names for an allowed auth + AuthenticationAllowed bool // when false, disallow this authentication attempt + RejectedAuthenticationMessage string // should be set when AuthenticationAllowed is false +} + +// IdentityTransformation is an individual identity transformation which can be evaluated. +type IdentityTransformation interface { + Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) +} + +// TransformationPipeline is a list of identity transforms, which can be evaluated in order against some given input +// values. +type TransformationPipeline struct { + transforms []IdentityTransformation +} + +// NewTransformationPipeline creates an empty TransformationPipeline. +func NewTransformationPipeline() *TransformationPipeline { + return &TransformationPipeline{transforms: []IdentityTransformation{}} +} + +// AppendTransformation adds a transformation to the end of the list of transformations for this pipeline. +// This is not thread-safe, so be sure to add all transformations from a single goroutine before using Evaluate +// from multiple goroutines. +func (p *TransformationPipeline) AppendTransformation(t IdentityTransformation) { + p.transforms = append(p.transforms, t) +} + +// Evaluate runs the transformation pipeline for a given input identity. It returns a potentially transformed or +// rejected identity, or an error. If any transformation in the list rejects the authentication, then the list is +// short-circuited but no error is returned. Only unexpected errors are returned as errors. This is safe to call +// from multiple goroutines. +func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + accumulatedResult := &TransformationResult{ + Username: username, + Groups: groups, + AuthenticationAllowed: true, + } + var err error + for i, transform := range p.transforms { + accumulatedResult, err = transform.Evaluate(ctx, accumulatedResult.Username, accumulatedResult.Groups) + if err != nil { + // There was an unexpected error evaluating a transformation. + return nil, fmt.Errorf("identity transformation at index %d: %w", i, err) + } + if !accumulatedResult.AuthenticationAllowed { + // Auth has been rejected by a policy. Stop evaluating the rest of the transformations. + return accumulatedResult, nil + } + if strings.TrimSpace(accumulatedResult.Username) == "" { + return nil, fmt.Errorf("identity transformation returned an empty username, which is not allowed") + } + } + // There were no unexpected errors and no policy which rejected auth. + return accumulatedResult, nil +} From 1a53b4daea3635f00887c0c9cde692bb5a8b597d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 6 Feb 2023 17:04:59 -0800 Subject: [PATCH 03/81] Allow user-defined string & stringList consts for use in CEL expressions --- internal/celtransformer/celformer.go | 112 ++++++++++++++++------ internal/celtransformer/celformer_test.go | 108 ++++++++++++++++++++- 2 files changed, 188 insertions(+), 32 deletions(-) diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go index 409e064b8..133555a11 100644 --- a/internal/celtransformer/celformer.go +++ b/internal/celtransformer/celformer.go @@ -12,6 +12,7 @@ import ( "context" "fmt" "reflect" + "regexp" "strings" "time" @@ -23,8 +24,10 @@ import ( ) const ( - usernameVariableName = "username" - groupsVariableName = "groups" + usernameVariableName = "username" + groupsVariableName = "groups" + constStringVariableName = "strConst" + constStringListVariableName = "strListConst" defaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy" ) @@ -46,15 +49,51 @@ func NewCELTransformer(maxExpressionRuntime time.Duration) (*CELTransformer, err return &CELTransformer{compiler: env, maxExpressionRuntime: maxExpressionRuntime}, nil } +// TransformationConstants can be used to make more variables available to compiled CEL expressions for convenience. +type TransformationConstants struct { + // A map of variable names to their string values. If a key "x" has value "123", then it will be available + // to CEL expressions as the variable `strConst.x` with value `"123"`. + StringConstants map[string]string + // A map of variable names to their string list values. If a key "x" has value []string{"123","456"}, + // then it will be available to CEL expressions as the variable `strListConst.x` with value `["123","456"]`. + StringListConstants map[string][]string +} + +// Valid identifiers in CEL expressions are defined by the CEL language spec as: [_a-zA-Z][_a-zA-Z0-9]* +var validIdentifiersRegexp = regexp.MustCompile(`^[_a-zA-Z][_a-zA-Z0-9]*$`) + +func (t *TransformationConstants) validateVariableNames() error { + const errFormat = "%q is an invalid const variable name (must match [_a-zA-Z][_a-zA-Z0-9]*)" + for k := range t.StringConstants { + if !validIdentifiersRegexp.MatchString(k) { + return fmt.Errorf(errFormat, k) + } + } + for k := range t.StringListConstants { + if !validIdentifiersRegexp.MatchString(k) { + return fmt.Errorf(errFormat, k) + } + } + return nil +} + // CompileTransformation compiles a CEL-based identity transformation expression. // The compiled transform can be cached in memory and executed repeatedly and in a thread-safe way. -func (c *CELTransformer) CompileTransformation(t CELTransformation) (idtransform.IdentityTransformation, error) { - return t.compile(c) +// The caller must not modify the consts param struct after calling this function to allow +// the returned IdentityTransformation to use it as a thread-safe read-only structure. +func (c *CELTransformer) CompileTransformation(t CELTransformation, consts *TransformationConstants) (idtransform.IdentityTransformation, error) { + if consts == nil { + consts = &TransformationConstants{} + } + if err := consts.validateVariableNames(); err != nil { + return nil, err + } + return t.compile(c, consts) } // CELTransformation can be compiled into an IdentityTransformation. type CELTransformation interface { - compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) + compile(transformer *CELTransformer, consts *TransformationConstants) (idtransform.IdentityTransformation, error) } // UsernameTransformation is a CEL expression that can transform a username (or leave it unchanged). @@ -108,76 +147,91 @@ func compileProgram(transformer *CELTransformer, expectedExpressionType *cel.Typ return program, nil } -func (t *UsernameTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { +func (t *UsernameTransformation) compile(transformer *CELTransformer, consts *TransformationConstants) (idtransform.IdentityTransformation, error) { program, err := compileProgram(transformer, cel.StringType, t.Expression) if err != nil { return nil, err } return &compiledUsernameTransformation{ - program: program, - maxExpressionRuntime: transformer.maxExpressionRuntime, + baseCompiledTransformation: &baseCompiledTransformation{ + program: program, + consts: consts, + maxExpressionRuntime: transformer.maxExpressionRuntime, + }, }, nil } -func (t *GroupsTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { +func (t *GroupsTransformation) compile(transformer *CELTransformer, consts *TransformationConstants) (idtransform.IdentityTransformation, error) { program, err := compileProgram(transformer, cel.ListType(cel.StringType), t.Expression) if err != nil { return nil, err } return &compiledGroupsTransformation{ - program: program, - maxExpressionRuntime: transformer.maxExpressionRuntime, + baseCompiledTransformation: &baseCompiledTransformation{ + program: program, + consts: consts, + maxExpressionRuntime: transformer.maxExpressionRuntime, + }, }, nil } -func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) { +func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer, consts *TransformationConstants) (idtransform.IdentityTransformation, error) { program, err := compileProgram(transformer, cel.BoolType, t.Expression) if err != nil { return nil, err } return &compiledAllowAuthenticationPolicy{ - program: program, - maxExpressionRuntime: transformer.maxExpressionRuntime, + baseCompiledTransformation: &baseCompiledTransformation{ + program: program, + consts: consts, + maxExpressionRuntime: transformer.maxExpressionRuntime, + }, rejectedAuthenticationMessage: t.RejectedAuthenticationMessage, }, nil } -// Implements idtransform.IdentityTransformation. -type compiledUsernameTransformation struct { +// Base type for common aspects of compiled transformations. +type baseCompiledTransformation struct { program cel.Program + consts *TransformationConstants maxExpressionRuntime time.Duration } +// Implements idtransform.IdentityTransformation. +type compiledUsernameTransformation struct { + *baseCompiledTransformation +} + // Implements idtransform.IdentityTransformation. type compiledGroupsTransformation struct { - program cel.Program - maxExpressionRuntime time.Duration + *baseCompiledTransformation } // Implements idtransform.IdentityTransformation. type compiledAllowAuthenticationPolicy struct { - program cel.Program - maxExpressionRuntime time.Duration + *baseCompiledTransformation rejectedAuthenticationMessage string } -func evalProgram(ctx context.Context, program cel.Program, maxExpressionRuntime time.Duration, username string, groups []string) (ref.Val, error) { +func (c *baseCompiledTransformation) evalProgram(ctx context.Context, username string, groups []string) (ref.Val, error) { // Limit the runtime of a CEL expression to avoid accidental very expensive expressions. - timeoutCtx, cancel := context.WithTimeout(ctx, maxExpressionRuntime) + timeoutCtx, cancel := context.WithTimeout(ctx, c.maxExpressionRuntime) defer cancel() // Evaluation is thread-safe and side effect free. Many inputs can be sent to the same cel.Program // and if fields are present in the input, but not referenced in the expression, they are ignored. // The argument to Eval may either be an `interpreter.Activation` or a `map[string]interface{}`. - val, _, err := program.ContextEval(timeoutCtx, map[string]interface{}{ - usernameVariableName: username, - groupsVariableName: groups, + val, _, err := c.program.ContextEval(timeoutCtx, map[string]interface{}{ + usernameVariableName: username, + groupsVariableName: groups, + constStringVariableName: c.consts.StringConstants, + constStringListVariableName: c.consts.StringListConstants, }) return val, err } func (c *compiledUsernameTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { - val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + val, err := c.evalProgram(ctx, username, groups) if err != nil { return nil, err } @@ -197,7 +251,7 @@ func (c *compiledUsernameTransformation) Evaluate(ctx context.Context, username } func (c *compiledGroupsTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { - val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + val, err := c.evalProgram(ctx, username, groups) if err != nil { return nil, err } @@ -217,7 +271,7 @@ func (c *compiledGroupsTransformation) Evaluate(ctx context.Context, username st } func (c *compiledAllowAuthenticationPolicy) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) { - val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups) + val, err := c.evalProgram(ctx, username, groups) if err != nil { return nil, err } @@ -254,6 +308,8 @@ func newEnv() (*cel.Env, error) { // the parsing/checking phase. cel.Variable(usernameVariableName, cel.StringType), cel.Variable(groupsVariableName, cel.ListType(cel.StringType)), + cel.Variable(constStringVariableName, cel.MapType(cel.StringType, cel.StringType)), + cel.Variable(constStringListVariableName, cel.MapType(cel.StringType, cel.ListType(cel.StringType))), // Enable the strings extensions. // See https://github.com/google/cel-go/tree/master/ext#strings diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 762f8ecf3..54724a416 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -30,6 +30,7 @@ func TestTransformer(t *testing.T) { username string groups []string transforms []CELTransformation + consts *TransformationConstants ctx context.Context wantUsername string @@ -113,6 +114,28 @@ func TestTransformer(t *testing.T) { wantUsername: "other", wantGroups: []string{"admins", "developers", "other", "ryan", "other2"}, }, + { + name: "any transformation can use the provided constants as variables", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + consts: &TransformationConstants{ + StringConstants: map[string]string{ + "x": "abc", + "y": "def", + }, + StringListConstants: map[string][]string{ + "x": {"uvw", "xyz"}, + "y": {"123", "456"}, + }, + }, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `strConst.x + strListConst.x[0]`}, + &GroupsTransformation{Expression: `[strConst.x, strConst.y, strListConst.x[1], strListConst.y[0]]`}, + &AllowAuthenticationPolicy{Expression: `strConst.x == "abc"`}, + }, + wantUsername: "abcuvw", + wantGroups: []string{"abc", "def", "xyz", "123"}, + }, { name: "the CEL string extensions are enabled for use in the expressions", username: " ryan ", @@ -219,6 +242,19 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"admins", "developers"}, }, + { + name: "can filter groups based on an allow list provided as a const", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + consts: &TransformationConstants{ + StringListConstants: map[string][]string{"allowedGroups": {"admins", "developers"}}, + }, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(g, g in strListConst.allowedGroups)`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers"}, + }, { name: "can filter groups based on a disallow list", username: "ryan", @@ -239,6 +275,19 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"other"}, }, + { + name: "can filter groups based on a disallowed prefixes provided as a const", + username: "ryan", + groups: []string{"disallowed1:admins", "disallowed2:developers", "other"}, + consts: &TransformationConstants{ + StringListConstants: map[string][]string{"disallowedPrefixes": {"disallowed1:", "disallowed2:"}}, + }, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups.filter(group, !(strListConst.disallowedPrefixes.exists(prefix, group.startsWith(prefix))))`}, + }, + wantUsername: "ryan", + wantGroups: []string{"other"}, + }, { name: "can add a group", username: "ryan", @@ -249,6 +298,19 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"admins", "developers", "other", "new-group"}, }, + { + name: "can add a group from a const", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + consts: &TransformationConstants{ + StringConstants: map[string]string{"groupToAlwaysAdd": "new-group"}, + }, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups + [strConst.groupToAlwaysAdd]`}, + }, + wantUsername: "ryan", + wantGroups: []string{"admins", "developers", "other", "new-group"}, + }, { name: "can add a group but only if they already belong to another group - when the user does belong to that other group", username: "ryan", @@ -622,6 +684,44 @@ func TestTransformer(t *testing.T) { }, wantCompileErr: `CEL expression should return type "list(string)" but returns type "list(dyn)"`, }, + { + name: "using string constants which were not were provided", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `strConst.x`}, + }, + wantEvaluationErr: `identity transformation at index 0: no such key: x`, + }, + { + name: "using string list constants which were not were provided", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `strListConst.x`}, + }, + wantEvaluationErr: `identity transformation at index 0: no such key: x`, + }, + { + name: "using an illegal name for a string constant", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + consts: &TransformationConstants{StringConstants: map[string]string{" illegal": "a"}}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username`}, + }, + wantCompileErr: `" illegal" is an invalid const variable name (must match [_a-zA-Z][_a-zA-Z0-9]*)`, + }, + { + name: "using an illegal name for a stringList constant", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + consts: &TransformationConstants{StringListConstants: map[string][]string{" illegal": {"a"}}}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username`}, + }, + wantCompileErr: `" illegal" is an invalid const variable name (must match [_a-zA-Z][_a-zA-Z0-9]*)`, + }, } for _, tt := range tests { @@ -635,7 +735,7 @@ func TestTransformer(t *testing.T) { pipeline := idtransform.NewTransformationPipeline() for _, transform := range tt.transforms { - compiledTransform, err := transformer.CompileTransformation(transform) + compiledTransform, err := transformer.CompileTransformation(transform, tt.consts) if tt.wantCompileErr != "" { require.EqualError(t, err, tt.wantCompileErr) return // the rest of the test doesn't make sense when there was a compile error @@ -673,13 +773,13 @@ func TestTypicalPerformanceAndThreadSafety(t *testing.T) { pipeline := idtransform.NewTransformationPipeline() var compiledTransform idtransform.IdentityTransformation - compiledTransform, err = transformer.CompileTransformation(&UsernameTransformation{Expression: `"username_prefix:" + username`}) + compiledTransform, err = transformer.CompileTransformation(&UsernameTransformation{Expression: `"username_prefix:" + username`}, nil) require.NoError(t, err) pipeline.AppendTransformation(compiledTransform) - compiledTransform, err = transformer.CompileTransformation(&GroupsTransformation{Expression: `groups.map(g, "group_prefix:" + g)`}) + compiledTransform, err = transformer.CompileTransformation(&GroupsTransformation{Expression: `groups.map(g, "group_prefix:" + g)`}, nil) require.NoError(t, err) pipeline.AppendTransformation(compiledTransform) - compiledTransform, err = transformer.CompileTransformation(&AllowAuthenticationPolicy{Expression: `username == "username_prefix:ryan"`}) + compiledTransform, err = transformer.CompileTransformation(&AllowAuthenticationPolicy{Expression: `username == "username_prefix:ryan"`}, nil) require.NoError(t, err) pipeline.AppendTransformation(compiledTransform) From 7af75dfe3c775ca924a27502f42b4d75ccca84a5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 8 May 2023 14:07:38 -0700 Subject: [PATCH 04/81] First draft of implementation of multiple IDPs support --- hack/prepare-supervisor-on-kind.sh | 245 ++++++++++---- .../active_directory_upstream_watcher.go | 18 +- .../active_directory_upstream_watcher_test.go | 41 +-- .../federation_domain_watcher.go | 299 +++++++++++++++++- .../ldap_upstream_watcher.go | 8 +- .../ldap_upstream_watcher_test.go | 3 +- .../oidc_upstream_watcher.go | 8 +- .../oidc_upstream_watcher_test.go | 5 +- .../upstreamwatchers/upstream_watchers.go | 4 +- .../supervisorstorage/garbage_collector.go | 11 +- .../garbage_collector_test.go | 21 +- internal/controller/utils.go | 12 +- .../fositestorage/accesstoken/accesstoken.go | 5 +- .../authorizationcode/authorizationcode.go | 5 +- .../openidconnect/openidconnect.go | 5 +- internal/fositestorage/pkce/pkce.go | 5 +- .../refreshtoken/refreshtoken.go | 5 +- .../generate.go | 4 +- .../mockupstreamoidcidentityprovider.go | 6 +- internal/oidc/auth/auth_handler.go | 167 ++++++---- internal/oidc/auth/auth_handler_test.go | 4 +- internal/oidc/callback/callback_handler.go | 25 +- .../downstreamsession/downstream_session.go | 68 +++- .../idpdiscovery/idp_discovery_handler.go | 22 +- .../idp_discovery_handler_test.go | 10 +- .../oidc/idplister/upstream_idp_lister.go | 26 ++ internal/oidc/login/get_login_handler_test.go | 5 +- internal/oidc/login/post_login_handler.go | 32 +- internal/oidc/oidc.go | 58 +--- .../provider/dynamic_upstream_idp_provider.go | 153 ++------- ...ration_domain_identity_providers_lister.go | 232 ++++++++++++++ .../oidc/provider/federation_domain_issuer.go | 40 ++- internal/oidc/provider/manager/manager.go | 57 ++-- .../upstreamprovider/upsteam_provider.go | 126 ++++++++ internal/oidc/token/token_handler.go | 188 +++++++---- internal/psession/pinniped_session.go | 19 +- internal/supervisor/server/server.go | 3 + .../testutil/oidctestutil/oidctestutil.go | 29 +- internal/upstreamldap/upstreamldap.go | 8 +- internal/upstreamldap/upstreamldap_test.go | 16 +- internal/upstreamoidc/upstreamoidc.go | 9 +- internal/upstreamoidc/upstreamoidc_test.go | 37 +-- pkg/oidcclient/login.go | 17 +- pkg/oidcclient/login_test.go | 30 +- 44 files changed, 1465 insertions(+), 626 deletions(-) create mode 100644 internal/oidc/idplister/upstream_idp_lister.go create mode 100644 internal/oidc/provider/federation_domain_identity_providers_lister.go create mode 100644 internal/oidc/provider/upstreamprovider/upsteam_provider.go diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index a56e59709..e8c937d06 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +# Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -81,7 +81,7 @@ while (("$#")); do done if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then - log_error "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like" + log_error "Error: Please use --oidc, --ldap, or --ad to specify which type(s) of upstream identity provider(s) you would like. May use one or multiple." exit 1 fi @@ -103,42 +103,6 @@ audience="my-workload-cluster-$(openssl rand -hex 4)" issuer_host="pinniped-supervisor-clusterip.supervisor.svc.cluster.local" issuer="https://$issuer_host/some/path" -# Create a CA and TLS serving certificates for the Supervisor. -step certificate create \ - "Supervisor CA" "$root_ca_crt_path" "$root_ca_key_path" \ - --profile root-ca \ - --no-password --insecure --force -step certificate create \ - "$issuer_host" "$tls_crt_path" "$tls_key_path" \ - --profile leaf \ - --not-after 8760h \ - --ca "$root_ca_crt_path" --ca-key "$root_ca_key_path" \ - --no-password --insecure --force - -# Put the TLS certificate into a Secret for the Supervisor. -kubectl create secret tls -n "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" my-federation-domain-tls --cert "$tls_crt_path" --key "$tls_key_path" \ - --dry-run=client --output yaml | kubectl apply -f - - -# Make a FederationDomain using the TLS Secret from above. -cat <kubeconfig +if [[ "$use_oidc_upstream" == "yes" ]]; then + https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" \ + ./pinniped get kubeconfig --oidc-skip-browser $flow_arg --upstream-identity-provider-type oidc >kubeconfig-oidc.yaml +fi +if [[ "$use_ldap_upstream" == "yes" ]]; then + https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" \ + ./pinniped get kubeconfig --oidc-skip-browser $flow_arg --upstream-identity-provider-type ldap >kubeconfig-ldap.yaml +fi +if [[ "$use_ad_upstream" == "yes" ]]; then + https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" \ + ./pinniped get kubeconfig --oidc-skip-browser $flow_arg --upstream-identity-provider-type activedirectory >kubeconfig-ad.yaml +fi # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. rm -f "$HOME/.config/pinniped/sessions.yaml" @@ -304,37 +420,48 @@ echo "Ready! 🚀" if [[ "$use_oidc_upstream" == "yes" || "$use_flow" == "browser_authcode" ]]; then echo - echo "To be able to access the login URL shown below, start Chrome like this:" + echo "To be able to access the Supervisor URL during login, start Chrome like this:" echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\"" echo "Note that Chrome must be fully quit before being started with --proxy-server." echo "Then open the login URL shown below in that new Chrome window." echo echo "When prompted for username and password, use these values:" + echo fi if [[ "$use_oidc_upstream" == "yes" ]]; then - echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" - echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" + echo " OIDC Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" + echo " OIDC Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" + echo fi if [[ "$use_ldap_upstream" == "yes" ]]; then - echo " Username: $PINNIPED_TEST_LDAP_USER_CN" - echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" + echo " LDAP Username: $PINNIPED_TEST_LDAP_USER_CN" + echo " LDAP Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" + echo fi if [[ "$use_ad_upstream" == "yes" ]]; then - echo " Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME" - echo " Password: $PINNIPED_TEST_AD_USER_PASSWORD" + echo " AD Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME" + echo " AD Password: $PINNIPED_TEST_AD_USER_PASSWORD" + echo fi -# Perform a login using the kubectl plugin. This should print the URL to be followed for the Dex login page -# if using an OIDC upstream, or should prompt on the CLI for username/password if using an LDAP upstream. -echo -echo "Running: PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" kubectl --kubeconfig ./kubeconfig get pods -A" -PINNIPED_DEBUG=true https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" kubectl --kubeconfig ./kubeconfig get pods -A - -# Print the identity of the currently logged in user. The CLI has cached your tokens, and will automatically refresh -# your short-lived credentials whenever they expire, so you should not be prompted to log in again for the rest of the day. -echo -echo "Running: PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig" -PINNIPED_DEBUG=true https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped whoami --kubeconfig ./kubeconfig +# Echo the commands that may be used to login and print the identity of the currently logged in user. +# Once the CLI has cached your tokens, it will automatically refresh your short-lived credentials whenever +# they expire, so you should not be prompted to log in again for the rest of the day. +if [[ "$use_oidc_upstream" == "yes" ]]; then + echo "To log in using OIDC, run:" + echo "PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig-oidc.yaml" + echo +fi +if [[ "$use_ldap_upstream" == "yes" ]]; then + echo "To log in using LDAP, run:" + echo "PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig-ldap.yaml" + echo +fi +if [[ "$use_ad_upstream" == "yes" ]]; then + echo "To log in using AD, run:" + echo "PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig-ad.yaml" + echo +fi diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index f2d658f63..b1f214968 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -26,7 +26,7 @@ import ( "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -225,7 +225,7 @@ func (s *activeDirectoryUpstreamGenericLDAPStatus) Conditions() []metav1.Conditi // UpstreamActiveDirectoryIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamActiveDirectoryIdentityProviderICache interface { - SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) + SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI) } type activeDirectoryWatcherController struct { @@ -299,7 +299,7 @@ func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error } requeue := false - validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) + validatedUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) for _, upstream := range actualUpstreams { valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream) if valid != nil { @@ -318,7 +318,7 @@ func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error return nil } -func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { +func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p upstreamprovider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec adUpstreamImpl := &activeDirectoryUpstreamGenericLDAPImpl{activeDirectoryIdentityProvider: *upstream} @@ -344,7 +344,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){ "objectGUID": microsoftUUIDFromBinaryAttr("objectGUID"), }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ pwdLastSetAttribute: attributeUnchangedSinceLogin(pwdLastSetAttribute), userAccountControlAttribute: validUserAccountControl, userAccountControlComputedAttribute: validComputedUserAccountControl, @@ -445,7 +445,7 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) { } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var validUserAccountControl = func(entry *ldap.Entry, _ provider.RefreshAttributes) error { +var validUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.RefreshAttributes) error { userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute)) if err != nil { return err @@ -459,7 +459,7 @@ var validUserAccountControl = func(entry *ldap.Entry, _ provider.RefreshAttribut } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var validComputedUserAccountControl = func(entry *ldap.Entry, _ provider.RefreshAttributes) error { +var validComputedUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.RefreshAttributes) error { userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlComputedAttribute)) if err != nil { return err @@ -473,8 +473,8 @@ var validComputedUserAccountControl = func(entry *ldap.Entry, _ provider.Refresh } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var attributeUnchangedSinceLogin = func(attribute string) func(*ldap.Entry, provider.RefreshAttributes) error { - return func(entry *ldap.Entry, storedAttributes provider.RefreshAttributes) error { +var attributeUnchangedSinceLogin = func(attribute string) func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { + return func(entry *ldap.Entry, storedAttributes upstreamprovider.RefreshAttributes) error { prevAttributeValue := storedAttributes.AdditionalAttributes[attribute] newValues := entry.GetRawAttributeValues(attribute) 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 bb830aa29..c223441d7 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -31,6 +31,7 @@ import ( "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/upstreamldap" ) @@ -229,7 +230,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -572,7 +573,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -642,7 +643,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: "sAMAccountName", }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -715,7 +716,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -795,7 +796,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -859,7 +860,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1010,7 +1011,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1160,7 +1161,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1232,7 +1233,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1499,7 +1500,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": groupSAMAccountNameWithDomainSuffix}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1559,7 +1560,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1623,7 +1624,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1687,7 +1688,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1899,7 +1900,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1962,7 +1963,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { SkipGroupRefresh: true, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -2010,7 +2011,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + cache.SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) @@ -2104,8 +2105,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks - copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.RefreshAttributes) error{} - actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.RefreshAttributes) error{} + copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{} + actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{} require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks)) for k, v := range expectedRefreshAttributeChecks { require.NotNil(t, actualRefreshAttributeChecks[k]) @@ -2354,7 +2355,7 @@ func TestValidUserAccountControl(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - err := validUserAccountControl(tt.entry, provider.RefreshAttributes{}) + err := validUserAccountControl(tt.entry, upstreamprovider.RefreshAttributes{}) if tt.wantErr != "" { require.Error(t, err) @@ -2415,7 +2416,7 @@ func TestValidComputedUserAccountControl(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - err := validComputedUserAccountControl(tt.entry, provider.RefreshAttributes{}) + err := validComputedUserAccountControl(tt.entry, upstreamprovider.RefreshAttributes{}) if tt.wantErr != "" { require.Error(t, err) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 08492e17e..9df425086 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorconfig @@ -8,9 +8,11 @@ import ( "fmt" "net/url" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" @@ -19,8 +21,11 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" configinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/config/v1alpha1" + idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" + "go.pinniped.dev/internal/celtransformer" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" ) @@ -33,10 +38,14 @@ type ProvidersSetter interface { } type federationDomainWatcherController struct { - providerSetter ProvidersSetter - clock clock.Clock - client pinnipedclientset.Interface - federationDomainInformer configinformers.FederationDomainInformer + providerSetter ProvidersSetter + clock clock.Clock + client pinnipedclientset.Interface + + federationDomainInformer configinformers.FederationDomainInformer + oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer + ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer + activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer } // NewFederationDomainWatcherController creates a controllerlib.Controller that watches @@ -46,16 +55,22 @@ func NewFederationDomainWatcherController( clock clock.Clock, client pinnipedclientset.Interface, federationDomainInformer configinformers.FederationDomainInformer, + oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer, + ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, + activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return controllerlib.New( controllerlib.Config{ Name: "FederationDomainWatcherController", Syncer: &federationDomainWatcherController{ - providerSetter: providerSetter, - clock: clock, - client: client, - federationDomainInformer: federationDomainInformer, + providerSetter: providerSetter, + clock: clock, + client: client, + federationDomainInformer: federationDomainInformer, + oidcIdentityProviderInformer: oidcIdentityProviderInformer, + ldapIdentityProviderInformer: ldapIdentityProviderInformer, + activeDirectoryIdentityProviderInformer: activeDirectoryIdentityProviderInformer, }, }, withInformer( @@ -63,6 +78,27 @@ func NewFederationDomainWatcherController( pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), + withInformer( + oidcIdentityProviderInformer, + // Since this controller only cares about IDP metadata names and UIDs (immutable fields), + // we only need to trigger Sync on creates and deletes. + pinnipedcontroller.MatchAnythingIgnoringUpdatesFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + ldapIdentityProviderInformer, + // Since this controller only cares about IDP metadata names and UIDs (immutable fields), + // we only need to trigger Sync on creates and deletes. + pinnipedcontroller.MatchAnythingIgnoringUpdatesFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + activeDirectoryIdentityProviderInformer, + // Since this controller only cares about IDP metadata names and UIDs (immutable fields), + // we only need to trigger Sync on creates and deletes. + pinnipedcontroller.MatchAnythingIgnoringUpdatesFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), ) } @@ -143,8 +179,239 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro continue } - federationDomainIssuer, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer) // This validates the Issuer URL. + // TODO: Move all this identity provider stuff into helper functions. This is just a sketch of how the code would + // work in the sense that this is not doing error handling, is not validating everything that it should, and + // is not updating the status of the FederationDomain with anything related to these identity providers. + // This code may crash on invalid inputs since it is not handling any errors. However, when given valid inputs, + // this correctly implements the multiple IDPs features. + // Create the list of IDPs for this FederationDomain. + // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded + // into the provider cache, so they cannot actually be used to authenticate. + federationDomainIdentityProviders := []*provider.FederationDomainIdentityProvider{} + var defaultFederationDomainIdentityProvider *provider.FederationDomainIdentityProvider + if len(federationDomain.Spec.IdentityProviders) == 0 { + // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. + oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) + ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) + activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) + // TODO handle err return value for each of the above three lines + + // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. + idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) + if idpCRsCount == 1 { + // If so, default that IDP's DisplayName to be the same as its resource Name. + defaultFederationDomainIdentityProvider = &provider.FederationDomainIdentityProvider{} + switch { + case len(oidcIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID + case len(ldapIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID + case len(activeDirectoryIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID + } + // Backwards compatibility mode always uses an empty identity transformation pipline since no + // transformations are defined on the FederationDomain. + defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() + plog.Warning("detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication", + "federationDomain", federationDomain.Name) + } else { + // There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards + // compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no + // IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore + // cannot actually be used to log in, but still serves endpoints. + // TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it. + plog.Warning("FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled", + "federationDomain", federationDomain.Name, + "namespace", federationDomain.Namespace, + "identityProvidersCustomResourcesCount", idpCRsCount, + ) + } + } + + // If there is an explicit list of IDPs on the FederationDomain, then process the list. + celTransformer, _ := celtransformer.NewCELTransformer(time.Second) // TODO: what is a good duration limit here? + // TODO: handle err + for _, idp := range federationDomain.Spec.IdentityProviders { + var idpResourceUID types.UID + var idpResourceName string + // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. + // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" + // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself + // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef + // that does not resolve, put an error on the FederationDomain status. + switch idp.ObjectRef.Kind { + case "LDAPIdentityProvider": + ldapIDP, _ := c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + // TODO: handle notfound err and also unexpected errors + idpResourceName = ldapIDP.Name + idpResourceUID = ldapIDP.UID + case "ActiveDirectoryIdentityProvider": + adIDP, _ := c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + // TODO: handle notfound err and also unexpected errors + idpResourceName = adIDP.Name + idpResourceUID = adIDP.UID + case "OIDCIdentityProvider": + oidcIDP, _ := c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + // TODO: handle notfound err and also unexpected errors + idpResourceName = oidcIDP.Name + idpResourceUID = oidcIDP.UID + default: + // TODO: handle bad user input + } + plog.Debug("resolved identity provider object reference", + "kind", idp.ObjectRef.Kind, + "name", idp.ObjectRef.Name, + "foundResourceName", idpResourceName, + "foundResourceUID", idpResourceUID, + ) + + // Prepare the transformations. + pipeline := idtransform.NewTransformationPipeline() + consts := &celtransformer.TransformationConstants{ + StringConstants: map[string]string{}, + StringListConstants: map[string][]string{}, + } + // Read all the declared constants. + for _, c := range idp.Transforms.Constants { + switch c.Type { + case "string": + consts.StringConstants[c.Name] = c.StringValue + case "stringList": + consts.StringListConstants[c.Name] = c.StringListValue + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + } + } + // Compile all the expressions and add them to the pipeline. + for idx, e := range idp.Transforms.Expressions { + var rawTransform celtransformer.CELTransformation + switch e.Type { + case "username/v1": + rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} + case "groups/v1": + rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} + case "policy/v1": + rawTransform = &celtransformer.AllowAuthenticationPolicy{ + Expression: e.Expression, + RejectedAuthenticationMessage: e.Message, + } + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + } + compiledTransform, err := celTransformer.CompileTransformation(rawTransform, consts) + if err != nil { + // TODO: handle compile err + plog.Error("error compiling identity transformation", err, + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "transformationIndex", idx, + "transformationType", e.Type, + "transformationExpression", e.Expression, + ) + } + pipeline.AppendTransformation(compiledTransform) + plog.Debug("successfully compiled identity transformation expression", + "type", e.Type, + "expr", e.Expression, + "policyMessage", e.Message, + ) + } + // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. + for idx, e := range idp.Transforms.Examples { + // TODO: use a real context param below + result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) + // TODO: handle err + resultWasAuthRejected := !result.AuthenticationAllowed + if e.Expects.Rejected && !resultWasAuthRejected { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if !e.Expects.Rejected && resultWasAuthRejected { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { + // TODO: when expected message is blank, then treat it like it expects the default message + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if result.AuthenticationAllowed { + // In the case where the user expected the auth to be allowed and it was allowed, then compare + // the expected username and group names to the actual username and group names. + // TODO: when both of these fail, put both errors onto the status (not just the first one) + if e.Expects.Username != result.Username { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedUsername", e.Expects.Username, + "actualUsernameResult", result.Username, + ) + } + if !stringSlicesEqual(e.Expects.Groups, result.Groups) { + // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? + // TODO: What happens if the user did not write any group expectation? Treat it like expecting any empty list of groups? + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", + "federationDomain", federationDomain.Name, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedGroups", e.Expects.Groups, + "actualGroupsResult", result.Groups, + ) + } + } + } + // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. + federationDomainIdentityProviders = append(federationDomainIdentityProviders, &provider.FederationDomainIdentityProvider{ + DisplayName: idp.DisplayName, + UID: idpResourceUID, + Transforms: pipeline, + }) + plog.Debug("loaded FederationDomain identity provider", + "federationDomain", federationDomain.Name, + "identityProviderDisplayName", idp.DisplayName, + "identityProviderResourceUID", idpResourceUID, + ) + } + + // Now that we have the list of IDPs for this FederationDomain, create the issuer. + var federationDomainIssuer *provider.FederationDomainIssuer + err = nil + if defaultFederationDomainIdentityProvider != nil { + // This is the constructor for the backwards compatibility mode. + federationDomainIssuer, err = provider.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) + } else { + // This is the constructor for any other case, including when there is an empty list of IDPs. + federationDomainIssuer, err = provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) + } if err != nil { + // Note that the FederationDomainIssuer constructors validate the Issuer URL. if err := c.updateStatus( ctx.Context, federationDomain.Namespace, @@ -176,6 +443,18 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro return errors.NewAggregate(errs) } +func stringSlicesEqual(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for i, itemFromA := range a { + if b[i] != itemFromA { + return false + } + } + return true +} + func (c *federationDomainWatcherController) updateStatus( ctx context.Context, namespace, name string, diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index aa9ce940d..fcf3a7e35 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -20,7 +20,7 @@ import ( "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -133,7 +133,7 @@ func (s *ldapUpstreamGenericLDAPStatus) Conditions() []metav1.Condition { // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamLDAPIdentityProviderICache interface { - SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) + SetLDAPIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI) } type ldapWatcherController struct { @@ -207,7 +207,7 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { } requeue := false - validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) + validatedUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) for _, upstream := range actualUpstreams { valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream) if valid != nil { @@ -226,7 +226,7 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { return nil } -func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { +func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p upstreamprovider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec config := &upstreamldap.ProviderConfig{ diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index cc8e01884..46b0b1a8a 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -30,6 +30,7 @@ import ( "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/upstreamldap" ) @@ -1139,7 +1140,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + cache.SetLDAPIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 3cfbc7e25..f56e4fc92 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -35,7 +35,7 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" ) @@ -91,7 +91,7 @@ var ( // UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. type UpstreamOIDCIdentityProviderICache interface { - SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI) + SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI) } // lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration. @@ -175,13 +175,13 @@ func (c *oidcWatcherController) Sync(ctx controllerlib.Context) error { } requeue := false - validatedUpstreams := make([]provider.UpstreamOIDCIdentityProviderI, 0, len(actualUpstreams)) + validatedUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, 0, len(actualUpstreams)) for _, upstream := range actualUpstreams { valid := c.validateUpstream(ctx, upstream) if valid == nil { requeue = true } else { - validatedUpstreams = append(validatedUpstreams, provider.UpstreamOIDCIdentityProviderI(valid)) + validatedUpstreams = append(validatedUpstreams, upstreamprovider.UpstreamOIDCIdentityProviderI(valid)) } } c.cache.SetOIDCIdentityProviders(validatedUpstreams) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 7077cf57d..814b425ae 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -29,6 +29,7 @@ import ( "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -81,7 +82,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ + cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) secretInformer := kubeInformers.Core().V1().Secrets() @@ -1416,7 +1417,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) testLog := testlogger.NewLegacy(t) //nolint:staticcheck // old test with lots of log statements cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ + cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 1ab87787c..114a66b5b 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -16,7 +16,7 @@ import ( "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/constable" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -365,7 +365,7 @@ func validateAndSetLDAPServerConnectivityAndSearchBase( return ldapConnectionValidCondition, searchBaseFoundCondition } -func EvaluateConditions(conditions GradatedConditions, config *upstreamldap.ProviderConfig) (provider.UpstreamLDAPIdentityProviderI, bool) { +func EvaluateConditions(conditions GradatedConditions, config *upstreamldap.ProviderConfig) (upstreamprovider.UpstreamLDAPIdentityProviderI, bool) { for _, gradatedCondition := range conditions.gradatedConditions { if gradatedCondition.condition.Status != metav1.ConditionTrue && gradatedCondition.isFatal { // Invalid provider, so do not load it into the cache. diff --git a/internal/controller/supervisorstorage/garbage_collector.go b/internal/controller/supervisorstorage/garbage_collector.go index c11aa8c3f..0006f6875 100644 --- a/internal/controller/supervisorstorage/garbage_collector.go +++ b/internal/controller/supervisorstorage/garbage_collector.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorstorage @@ -27,6 +27,7 @@ import ( "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestorage/refreshtoken" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) @@ -43,7 +44,7 @@ type garbageCollectorController struct { // UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. type UpstreamOIDCIdentityProviderICache interface { - GetOIDCIdentityProviders() []provider.UpstreamOIDCIdentityProviderI + GetOIDCIdentityProviders() []upstreamprovider.UpstreamOIDCIdentityProviderI } func GarbageCollectorController( @@ -244,7 +245,7 @@ func (c *garbageCollectorController) tryRevokeUpstreamOIDCToken(ctx context.Cont } // Try to find the provider that was originally used to create the stored session. - var foundOIDCIdentityProviderI provider.UpstreamOIDCIdentityProviderI + var foundOIDCIdentityProviderI upstreamprovider.UpstreamOIDCIdentityProviderI for _, p := range c.idpCache.GetOIDCIdentityProviders() { if p.GetName() == customSessionData.ProviderName && p.GetResourceUID() == customSessionData.ProviderUID { foundOIDCIdentityProviderI = p @@ -260,7 +261,7 @@ func (c *garbageCollectorController) tryRevokeUpstreamOIDCToken(ctx context.Cont upstreamAccessToken := customSessionData.OIDC.UpstreamAccessToken if upstreamRefreshToken != "" { - err := foundOIDCIdentityProviderI.RevokeToken(ctx, upstreamRefreshToken, provider.RefreshTokenType) + err := foundOIDCIdentityProviderI.RevokeToken(ctx, upstreamRefreshToken, upstreamprovider.RefreshTokenType) if err != nil { return err } @@ -268,7 +269,7 @@ func (c *garbageCollectorController) tryRevokeUpstreamOIDCToken(ctx context.Cont } if upstreamAccessToken != "" { - err := foundOIDCIdentityProviderI.RevokeToken(ctx, upstreamAccessToken, provider.AccessTokenType) + err := foundOIDCIdentityProviderI.RevokeToken(ctx, upstreamAccessToken, upstreamprovider.AccessTokenType) if err != nil { return err } diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index d845e558e..2266725bc 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorstorage @@ -30,6 +30,7 @@ import ( "go.pinniped.dev/internal/fositestorage/refreshtoken" "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -369,7 +370,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -493,7 +494,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-access-token", - TokenType: provider.AccessTokenType, + TokenType: upstreamprovider.AccessTokenType, }, ) @@ -785,7 +786,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -810,7 +811,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -889,7 +890,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -1012,7 +1013,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -1136,7 +1137,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-access-token", - TokenType: provider.AccessTokenType, + TokenType: upstreamprovider.AccessTokenType, }, ) @@ -1214,7 +1215,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-refresh-token", - TokenType: provider.RefreshTokenType, + TokenType: upstreamprovider.RefreshTokenType, }, ) @@ -1291,7 +1292,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { &oidctestutil.RevokeTokenArgs{ Ctx: syncContext.Context, Token: "fake-upstream-access-token", - TokenType: provider.AccessTokenType, + TokenType: upstreamprovider.AccessTokenType, }, ) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 55e02d45f..6880fc2d7 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package controller @@ -18,6 +18,16 @@ func NameAndNamespaceExactMatchFilterFactory(name, namespace string) controllerl }, nil) } +// MatchAnythingIgnoringUpdatesFilter returns a controllerlib.Filter that allows all objects but ignores updates. +func MatchAnythingIgnoringUpdatesFilter(parentFunc controllerlib.ParentFunc) controllerlib.Filter { + return controllerlib.FilterFuncs{ + AddFunc: func(object metav1.Object) bool { return true }, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { return false }, + DeleteFunc: func(object metav1.Object) bool { return true }, + ParentFunc: parentFunc, + } +} + // MatchAnythingFilter returns a controllerlib.Filter that allows all objects. func MatchAnythingFilter(parentFunc controllerlib.ParentFunc) controllerlib.Filter { return SimpleFilter(func(object metav1.Object) bool { return true }, parentFunc) diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index a70f1a704..07b065ed4 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package accesstoken @@ -31,7 +31,8 @@ const ( // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. // Version 3 is when we added the Username field to the psession.CustomSessionData. // Version 4 is when fosite added json tags to their openid.DefaultSession struct. - accessTokenStorageVersion = "4" + // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. + accessTokenStorageVersion = "5" ) type RevocationStorage interface { diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index fd34ce2c1..abfa9e398 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package authorizationcode @@ -32,7 +32,8 @@ const ( // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. // Version 3 is when we added the Username field to the psession.CustomSessionData. // Version 4 is when fosite added json tags to their openid.DefaultSession struct. - authorizeCodeStorageVersion = "4" + // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. + authorizeCodeStorageVersion = "5" ) var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index d020e8859..4770e41f6 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package openidconnect @@ -32,7 +32,8 @@ const ( // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. // Version 3 is when we added the Username field to the psession.CustomSessionData. // Version 4 is when fosite added json tags to their openid.DefaultSession struct. - oidcStorageVersion = "4" + // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. + oidcStorageVersion = "5" ) var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index 92fe9f83d..3f44a00d3 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package pkce @@ -30,7 +30,8 @@ const ( // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. // Version 3 is when we added the Username field to the psession.CustomSessionData. // Version 4 is when fosite added json tags to their openid.DefaultSession struct. - pkceStorageVersion = "4" + // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. + pkceStorageVersion = "5" ) var _ pkce.PKCERequestStorage = &pkceStorage{} diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index 28c2b5cbf..9feaed559 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package refreshtoken @@ -31,7 +31,8 @@ const ( // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. // Version 3 is when we added the Username field to the psession.CustomSessionData. // Version 4 is when fosite added json tags to their openid.DefaultSession struct. - refreshTokenStorageVersion = "4" + // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. + refreshTokenStorageVersion = "5" ) type RevocationStorage interface { diff --git a/internal/mocks/mockupstreamoidcidentityprovider/generate.go b/internal/mocks/mockupstreamoidcidentityprovider/generate.go index cb9c46df5..d1ba84a07 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/generate.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/generate.go @@ -1,6 +1,6 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package mockupstreamoidcidentityprovider -//go:generate go run -v github.com/golang/mock/mockgen -destination=mockupstreamoidcidentityprovider.go -package=mockupstreamoidcidentityprovider -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/oidc/provider UpstreamOIDCIdentityProviderI +//go:generate go run -v github.com/golang/mock/mockgen -destination=mockupstreamoidcidentityprovider.go -package=mockupstreamoidcidentityprovider -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/oidc/provider/upstreamprovider UpstreamOIDCIdentityProviderI diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index 15a709a5c..cc66519f6 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -3,7 +3,7 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: go.pinniped.dev/internal/oidc/provider (interfaces: UpstreamOIDCIdentityProviderI) +// Source: go.pinniped.dev/internal/oidc/provider/upstreamprovider (interfaces: UpstreamOIDCIdentityProviderI) // Package mockupstreamoidcidentityprovider is a generated GoMock package. package mockupstreamoidcidentityprovider @@ -14,7 +14,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - provider "go.pinniped.dev/internal/oidc/provider" + upstreamprovider "go.pinniped.dev/internal/oidc/provider/upstreamprovider" nonce "go.pinniped.dev/pkg/oidcclient/nonce" oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes" pkce "go.pinniped.dev/pkg/oidcclient/pkce" @@ -245,7 +245,7 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) PerformRefresh(arg0, ar } // RevokeToken mocks base method. -func (m *MockUpstreamOIDCIdentityProviderI) RevokeToken(arg0 context.Context, arg1 string, arg2 provider.RevocableTokenType) error { +func (m *MockUpstreamOIDCIdentityProviderI) RevokeToken(arg0 context.Context, arg1 string, arg2 upstreamprovider.RevocableTokenType) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevokeToken", arg0, arg1, arg2) ret0, _ := ret[0].(error) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 772d1291d..7b94694fc 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -18,12 +18,14 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/login" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider/formposthtml" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -37,7 +39,7 @@ const ( func NewHandler( downstreamIssuer string, - idpLister oidc.UpstreamIdentityProvidersLister, + idpFinder provider.FederationDomainIdentityProvidersFinderI, oauthHelperWithoutStorage fosite.OAuth2Provider, oauthHelperWithStorage fosite.OAuth2Provider, generateCSRF func() (csrftoken.CSRFToken, error), @@ -57,20 +59,25 @@ func NewHandler( // Note that the client might have used oidcapi.AuthorizeUpstreamIDPNameParamName and // oidcapi.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP. // The Pinniped CLI has been sending these params since v0.9.0. - // Currently, these are ignored because the Supervisor does not yet support logins when multiple IDPs - // are configured. However, these params should be honored in the future when choosing an upstream - // here, e.g. by calling oidcapi.FindUpstreamIDPByNameAndType() when the params are present. - oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister) + idpNameQueryParamValue := r.URL.Query().Get(oidcapi.AuthorizeUpstreamIDPNameParamName) + oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpNameQueryParamValue, idpFinder) if err != nil { plog.WarningErr("authorize upstream config", err) return err } - if idpType == psession.ProviderTypeOIDC { + if oidcUpstream != nil { if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 || len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 { // The client set a username header, so they are trying to log in with a username/password. - return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) + return handleAuthRequestForOIDCUpstreamPasswordGrant( + r, + w, + oauthHelperWithStorage, + oidcUpstream.Provider, + oidcUpstream.Transforms, + idpNameQueryParamValue, + ) } return handleAuthRequestForOIDCUpstreamBrowserFlow(r, w, oauthHelperWithoutStorage, @@ -79,6 +86,7 @@ func NewHandler( downstreamIssuer, upstreamStateEncoder, cookieCodec, + idpNameQueryParamValue, ) } @@ -88,8 +96,10 @@ func NewHandler( // The client set a username header, so they are trying to log in with a username/password. return handleAuthRequestForLDAPUpstreamCLIFlow(r, w, oauthHelperWithStorage, - ldapUpstream, - idpType, + ldapUpstream.Provider, + ldapUpstream.SessionProviderType, + ldapUpstream.Transforms, + idpNameQueryParamValue, ) } return handleAuthRequestForLDAPUpstreamBrowserFlow( @@ -100,10 +110,11 @@ func NewHandler( generateNonce, generatePKCE, ldapUpstream, - idpType, + ldapUpstream.SessionProviderType, downstreamIssuer, upstreamStateEncoder, cookieCodec, + idpNameQueryParamValue, ) }) @@ -117,24 +128,28 @@ func handleAuthRequestForLDAPUpstreamCLIFlow( r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, - ldapUpstream provider.UpstreamLDAPIdentityProviderI, + ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, idpType psession.ProviderType, + identityTransforms *idtransform.TransformationPipeline, + idpNameQueryParamValue string, ) error { authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true) if !created { return nil } + maybeLogDeprecationWarningForMissingIDPParam(idpNameQueryParamValue, authorizeRequester) + if !requireStaticClientForUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) { return nil } - username, password, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) + submittedUsername, submittedPassword, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) if !hadUsernamePasswordValues { return nil } - authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes()) + authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes()) if err != nil { plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") @@ -146,9 +161,18 @@ func handleAuthRequestForLDAPUpstreamCLIFlow( } subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) - username = authenticateResponse.User.GetName() - groups := authenticateResponse.User.GetGroups() - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) + upstreamUsername := authenticateResponse.User.GetName() + upstreamGroups := authenticateResponse.User.GetGroups() + + username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), identityTransforms, upstreamUsername, upstreamGroups) + if err != nil { + oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, + fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, + ) + return nil + } + + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username, upstreamUsername, upstreamGroups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{}) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) @@ -163,11 +187,12 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), generatePKCE func() (pkce.Code, error), - ldapUpstream provider.UpstreamLDAPIdentityProviderI, + ldapUpstream *provider.FederationDomainResolvedLDAPIdentityProvider, idpType psession.ProviderType, downstreamIssuer string, upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, + idpNameQueryParamValue string, ) error { authRequestState, err := handleBrowserFlowAuthRequest( r, @@ -176,10 +201,11 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( generateCSRF, generateNonce, generatePKCE, - ldapUpstream.GetName(), + ldapUpstream.DisplayName, idpType, cookieCodec, upstreamStateEncoder, + idpNameQueryParamValue, ) if err != nil { return err @@ -196,18 +222,22 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, - oidcUpstream provider.UpstreamOIDCIdentityProviderI, + oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, + identityTransforms *idtransform.TransformationPipeline, + idpNameQueryParamValue string, ) error { authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true) if !created { return nil } + maybeLogDeprecationWarningForMissingIDPParam(idpNameQueryParamValue, authorizeRequester) + if !requireStaticClientForUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) { return nil } - username, password, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) + submittedUsername, submittedPassword, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) if !hadUsernamePasswordValues { return nil } @@ -220,7 +250,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } - token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password) + token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), submittedUsername, submittedPassword) if err != nil { // Upstream password grant errors can be generic errors (e.g. a network failure) or can be oauth2.RetrieveError errors // which represent the http response from the upstream server. These could be a 5XX or some other unexpected error, @@ -234,7 +264,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } - subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) + subject, upstreamUsername, upstreamGroups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) if err != nil { // Return a user-friendly error for this case which is entirely within our control. oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, @@ -243,9 +273,17 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } + username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), identityTransforms, upstreamUsername, upstreamGroups) + if err != nil { + oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, + fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, + ) + return nil + } + additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) - customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username) + customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username, upstreamUsername, upstreamGroups) if err != nil { oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, @@ -268,10 +306,11 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), generatePKCE func() (pkce.Code, error), - oidcUpstream provider.UpstreamOIDCIdentityProviderI, + oidcUpstream *provider.FederationDomainResolvedOIDCIdentityProvider, downstreamIssuer string, upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, + idpNameQueryParamValue string, ) error { authRequestState, err := handleBrowserFlowAuthRequest( r, @@ -280,10 +319,11 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( generateCSRF, generateNonce, generatePKCE, - oidcUpstream.GetName(), + oidcUpstream.DisplayName, psession.ProviderTypeOIDC, cookieCodec, upstreamStateEncoder, + idpNameQueryParamValue, ) if err != nil { return err @@ -294,12 +334,12 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( } upstreamOAuthConfig := oauth2.Config{ - ClientID: oidcUpstream.GetClientID(), + ClientID: oidcUpstream.Provider.GetClientID(), Endpoint: oauth2.Endpoint{ - AuthURL: oidcUpstream.GetAuthorizationURL().String(), + AuthURL: oidcUpstream.Provider.GetAuthorizationURL().String(), }, RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer), - Scopes: oidcUpstream.GetScopes(), + Scopes: oidcUpstream.Provider.GetScopes(), } authCodeOptions := []oauth2.AuthCodeOption{ @@ -308,7 +348,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( authRequestState.pkce.Method(), } - for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { + for key, val := range oidcUpstream.Provider.GetAdditionalAuthcodeParams() { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) } @@ -382,39 +422,31 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { // chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error. // Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. -func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) { - oidcUpstreams := idpLister.GetOIDCIdentityProviders() - ldapUpstreams := idpLister.GetLDAPIdentityProviders() - adUpstreams := idpLister.GetActiveDirectoryIdentityProviders() - switch { - case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0: - return nil, nil, "", httperr.New( - http.StatusUnprocessableEntity, - "No upstream providers are configured", - ) - case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) > 1: - var upstreamIDPNames []string - for _, idp := range oidcUpstreams { - upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) - } - for _, idp := range ldapUpstreams { - upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) - } - for _, idp := range adUpstreams { - upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) - } - plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) - return nil, nil, "", httperr.New( - http.StatusUnprocessableEntity, - "Too many upstream providers are configured (support for multiple upstreams is not yet implemented)", - ) - case len(oidcUpstreams) == 1: - return oidcUpstreams[0], nil, psession.ProviderTypeOIDC, nil - case len(adUpstreams) == 1: - return nil, adUpstreams[0], psession.ProviderTypeActiveDirectory, nil - default: - return nil, ldapUpstreams[0], psession.ProviderTypeLDAP, nil - } +func chooseUpstreamIDP(idpDisplayName string, idpLister provider.FederationDomainIdentityProvidersFinderI) (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { + // When a request is made to the authorization endpoint which does not specify the IDP name, then it might + // be an old dynamic client (OIDCClient). We need to make this work, but only in the backwards compatibility case + // where there is exactly one IDP defined in the namespace and no IDPs listed on the FederationDomain. + // This backwards compatibility mode is handled by FindDefaultIDP(). + if len(idpDisplayName) == 0 { + return idpLister.FindDefaultIDP() + } + return idpLister.FindUpstreamIDPByDisplayName(idpDisplayName) +} + +func maybeLogDeprecationWarningForMissingIDPParam(idpNameQueryParamValue string, authorizeRequester fosite.AuthorizeRequester) { + if len(idpNameQueryParamValue) != 0 { + return + } + plog.Warning("Client attempted to perform an authorization flow (user login) without specifying the "+ + "query param to choose an identity provider. "+ + "This will not work when identity providers are configured explicitly on a FederationDomain. "+ + "Additionally, this behavior is deprecated and support for any authorization requests missing this query param "+ + "may be removed in a future release. "+ + "Please ask the author of this client to update the authorization request URL to include this query parameter. "+ + "The value of the parameter should be equal to the displayName of the identity provider as declared in the FederationDomain.", + "missingParameterName", oidcapi.AuthorizeUpstreamIDPNameParamName, + "clientID", authorizeRequester.GetClient().GetID(), + ) } type browserFlowAuthRequestState struct { @@ -438,16 +470,19 @@ func handleBrowserFlowAuthRequest( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), generatePKCE func() (pkce.Code, error), - upstreamName string, + upstreamDisplayName string, idpType psession.ProviderType, cookieCodec oidc.Codec, upstreamStateEncoder oidc.Encoder, + idpNameQueryParamValue string, ) (*browserFlowAuthRequestState, error) { authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) if !created { return nil, nil // already wrote the error response, don't return error } + maybeLogDeprecationWarningForMissingIDPParam(idpNameQueryParamValue, authorizeRequester) + now := time.Now() _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ @@ -476,7 +511,7 @@ func handleBrowserFlowAuthRequest( encodedStateParamValue, err := upstreamStateParam( authorizeRequester, - upstreamName, + upstreamDisplayName, string(idpType), nonceValue, csrfValue, @@ -532,7 +567,7 @@ func generateValues( func upstreamStateParam( authorizeRequester fosite.AuthorizeRequester, - upstreamName string, + upstreamDisplayName string, upstreamType string, nonceValue nonce.Nonce, csrfValue csrftoken.CSRFToken, @@ -546,7 +581,7 @@ func upstreamStateParam( // The UpstreamName and UpstreamType struct fields can be used instead. // Remove those params here to avoid potential confusion about which should be used later. AuthParams: removeCustomIDPParams(authorizeRequester.GetRequestForm()).Encode(), - UpstreamName: upstreamName, + UpstreamName: upstreamDisplayName, UpstreamType: upstreamType, Nonce: nonceValue, CSRFToken: csrfValue, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index d2c8e262e..308adcc42 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -35,7 +35,7 @@ import ( "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/oidcclientvalidator" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -3287,7 +3287,7 @@ func TestAuthorizationEndpoint(t *testing.T) { WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}). WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123"}). Build() - idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) + idpLister.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{upstreamprovider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) // Update the expectations of the test case to match the new upstream IDP settings. test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index 05337b68d..e260d201c 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -20,7 +20,7 @@ import ( ) func NewHandler( - upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister, + upstreamIDPs provider.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider, stateDecoder, cookieDecoder oidc.Decoder, redirectURI string, @@ -31,11 +31,12 @@ func NewHandler( return err } - upstreamIDPConfig := findUpstreamIDPConfig(state.UpstreamName, upstreamIDPs) - if upstreamIDPConfig == nil { + resolvedOIDCIdentityProvider, _, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName) + if err != nil || resolvedOIDCIdentityProvider == nil { plog.Warning("upstream provider not found") return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found") } + upstreamIDPConfig := resolvedOIDCIdentityProvider.Provider downstreamAuthParams, err := url.ParseQuery(state.AuthParams) if err != nil { @@ -69,14 +70,19 @@ func NewHandler( return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") } - subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) + subject, upstreamUsername, upstreamGroups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) + if err != nil { + return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + } + + username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), resolvedOIDCIdentityProvider.Transforms, upstreamUsername, upstreamGroups) if err != nil { return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) - customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username) + customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username, upstreamUsername, upstreamGroups) if err != nil { return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } @@ -120,12 +126,3 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder) return decodedState, nil } - -func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister) provider.UpstreamOIDCIdentityProviderI { - for _, p := range upstreamIDPs.GetOIDCIdentityProviders() { - if p.GetName() == upstreamName { - return p - } - } - return nil -} diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/oidc/downstreamsession/downstream_session.go index 11f6c3407..f40bdff84 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/oidc/downstreamsession/downstream_session.go @@ -5,6 +5,7 @@ package downstreamsession import ( + "context" "errors" "fmt" "net/url" @@ -19,8 +20,9 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -38,6 +40,8 @@ const ( requiredClaimEmptyErr = constable.Error("required claim in upstream ID token is empty") emailVerifiedClaimInvalidFormatErr = constable.Error("email_verified claim in upstream ID token has invalid format") emailVerifiedClaimFalseErr = constable.Error("email_verified claim in upstream ID token has false value") + idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error") + idTransformPolicyErr = constable.Error("configured identity policy rejected this authentication") ) // MakeDownstreamSession creates a downstream OIDC session. @@ -82,16 +86,20 @@ func MakeDownstreamSession( } func MakeDownstreamLDAPOrADCustomSessionData( - ldapUpstream provider.UpstreamLDAPIdentityProviderI, + ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, idpType psession.ProviderType, authenticateResponse *authenticators.Response, username string, + untransformedUpstreamUsername string, + untransformedUpstreamGroups []string, ) *psession.CustomSessionData { customSessionData := &psession.CustomSessionData{ - Username: username, - ProviderUID: ldapUpstream.GetResourceUID(), - ProviderName: ldapUpstream.GetName(), - ProviderType: idpType, + Username: username, + UpstreamUsername: untransformedUpstreamUsername, + UpstreamGroups: untransformedUpstreamGroups, + ProviderUID: ldapUpstream.GetResourceUID(), + ProviderName: ldapUpstream.GetName(), + ProviderType: idpType, } if idpType == psession.ProviderTypeLDAP { @@ -112,9 +120,11 @@ func MakeDownstreamLDAPOrADCustomSessionData( } func MakeDownstreamOIDCCustomSessionData( - oidcUpstream provider.UpstreamOIDCIdentityProviderI, + oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token, username string, + untransformedUpstreamUsername string, + untransformedUpstreamGroups []string, ) (*psession.CustomSessionData, error) { upstreamSubject, err := ExtractStringClaimValue(oidcapi.IDTokenClaimSubject, oidcUpstream.GetName(), token.IDToken.Claims) if err != nil { @@ -126,10 +136,12 @@ func MakeDownstreamOIDCCustomSessionData( } customSessionData := &psession.CustomSessionData{ - Username: username, - ProviderUID: oidcUpstream.GetResourceUID(), - ProviderName: oidcUpstream.GetName(), - ProviderType: psession.ProviderTypeOIDC, + Username: username, + UpstreamUsername: untransformedUpstreamUsername, + UpstreamGroups: untransformedUpstreamGroups, + ProviderUID: oidcUpstream.GetResourceUID(), + ProviderName: oidcUpstream.GetName(), + ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamIssuer: upstreamIssuer, UpstreamSubject: upstreamSubject, @@ -200,7 +212,7 @@ func AutoApproveScopes(authorizeRequester fosite.AuthorizeRequester) { // GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order. func GetDownstreamIdentityFromUpstreamIDToken( - upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, idTokenClaims map[string]interface{}, ) (string, string, []string, error) { subject, username, err := getSubjectAndUsernameFromUpstreamIDToken(upstreamIDPConfig, idTokenClaims) @@ -218,7 +230,7 @@ func GetDownstreamIdentityFromUpstreamIDToken( // MapAdditionalClaimsFromUpstreamIDToken returns the additionalClaims mapped from the upstream token, if any. func MapAdditionalClaimsFromUpstreamIDToken( - upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, idTokenClaims map[string]interface{}, ) map[string]interface{} { mapped := make(map[string]interface{}, len(upstreamIDPConfig.GetAdditionalClaimMappings())) @@ -237,8 +249,32 @@ func MapAdditionalClaimsFromUpstreamIDToken( return mapped } +func ApplyIdentityTransformations( + ctx context.Context, + identityTransforms *idtransform.TransformationPipeline, + username string, + groups []string, +) (string, []string, error) { + transformationResult, err := identityTransforms.Evaluate(ctx, username, groups) + if err != nil { + plog.Error("unexpected identity transformation error during authentication", err, "inputUsername", username) + return "", nil, idTransformUnexpectedErr + } + if !transformationResult.AuthenticationAllowed { + plog.Debug("authentication rejected by configured policy", "inputUsername", username, "inputGroups", groups) + return "", nil, idTransformPolicyErr + } + plog.Debug("identity transformation successfully applied during authentication", + "originalUsername", username, + "newUsername", transformationResult.Username, + "originalGroups", groups, + "newGroups", transformationResult.Groups, + ) + return transformationResult.Username, transformationResult.Groups, nil +} + func getSubjectAndUsernameFromUpstreamIDToken( - upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, idTokenClaims map[string]interface{}, ) (string, string, error) { // The spec says the "sub" claim is only unique per issuer, @@ -323,7 +359,7 @@ func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl return valueAsString, nil } -func DownstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string { +func DownstreamSubjectFromUpstreamLDAP(ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string { ldapURL := *ldapUpstream.GetURL() return DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL) } @@ -343,7 +379,7 @@ func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSu // It returns nil when there is no configured groups claim name, or then when the configured claim name is not found // in the provided map of claims. It returns an error when the claim exists but its value cannot be parsed. func GetGroupsFromUpstreamIDToken( - upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, idTokenClaims map[string]interface{}, ) ([]string, error) { groupsClaimName := upstreamIDPConfig.GetGroupsClaim() diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/oidc/idpdiscovery/idp_discovery_handler.go index 66a974c9b..0b93b2b72 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package idpdiscovery provides a handler for the upstream IDP discovery endpoint. @@ -11,11 +11,11 @@ import ( "sort" "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" - "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/provider" ) // NewHandler returns an http.Handler that serves the upstream IDP discovery endpoint. -func NewHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) http.Handler { +func NewHandler(upstreamIDPs provider.FederationDomainIdentityProvidersListerI) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) @@ -36,31 +36,31 @@ func NewHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) http.Handler }) } -func responseAsJSON(upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, error) { +func responseAsJSON(upstreamIDPs provider.FederationDomainIdentityProvidersListerI) ([]byte, error) { r := v1alpha1.IDPDiscoveryResponse{PinnipedIDPs: []v1alpha1.PinnipedIDP{}} // The cache of IDPs could change at any time, so always recalculate the list. - for _, provider := range upstreamIDPs.GetLDAPIdentityProviders() { + for _, federationDomainIdentityProvider := range upstreamIDPs.GetLDAPIdentityProviders() { r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: provider.GetName(), + Name: federationDomainIdentityProvider.DisplayName, Type: v1alpha1.IDPTypeLDAP, Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, }) } - for _, provider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { + for _, federationDomainIdentityProvider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: provider.GetName(), + Name: federationDomainIdentityProvider.DisplayName, Type: v1alpha1.IDPTypeActiveDirectory, Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, }) } - for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { + for _, federationDomainIdentityProvider := range upstreamIDPs.GetOIDCIdentityProviders() { flows := []v1alpha1.IDPFlow{v1alpha1.IDPFlowBrowserAuthcode} - if provider.AllowsPasswordGrant() { + if federationDomainIdentityProvider.Provider.AllowsPasswordGrant() { flows = append(flows, v1alpha1.IDPFlowCLIPassword) } r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: provider.GetName(), + Name: federationDomainIdentityProvider.DisplayName, Type: v1alpha1.IDPTypeOIDC, Flows: flows, }) diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index b33ab2d84..7ab52d07d 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package idpdiscovery @@ -12,7 +12,7 @@ import ( "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil/oidctestutil" ) @@ -99,16 +99,16 @@ func TestIDPDiscovery(t *testing.T) { } // Change the list of IDPs in the cache. - idpLister.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + idpLister.SetLDAPIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-1"}, &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-2"}, }) - idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ + idpLister.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-1", AllowPasswordGrant: true}, &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, }) - idpLister.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + idpLister.SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-2"}, &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-1"}, }) diff --git a/internal/oidc/idplister/upstream_idp_lister.go b/internal/oidc/idplister/upstream_idp_lister.go new file mode 100644 index 000000000..3ea4ea649 --- /dev/null +++ b/internal/oidc/idplister/upstream_idp_lister.go @@ -0,0 +1,26 @@ +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package idplister + +import ( + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" +) + +type UpstreamOIDCIdentityProvidersLister interface { + GetOIDCIdentityProviders() []upstreamprovider.UpstreamOIDCIdentityProviderI +} + +type UpstreamLDAPIdentityProvidersLister interface { + GetLDAPIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI +} + +type UpstreamActiveDirectoryIdentityProviderLister interface { + GetActiveDirectoryIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI +} + +type UpstreamIdentityProvidersLister interface { + UpstreamOIDCIdentityProvidersLister + UpstreamLDAPIdentityProvidersLister + UpstreamActiveDirectoryIdentityProviderLister +} diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index bb85b8f27..30567309c 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/idplister" "go.pinniped.dev/internal/oidc/login/loginhtml" "go.pinniped.dev/internal/testutil" ) @@ -28,7 +29,7 @@ func TestGetLogin(t *testing.T) { decodedState *oidc.UpstreamStateParamData encodedState string errParam string - idps oidc.UpstreamIdentityProvidersLister + idps idplister.UpstreamIdentityProvidersLister wantStatus int wantContentType string wantBody string diff --git a/internal/oidc/login/post_login_handler.go b/internal/oidc/login/post_login_handler.go index 49f9c5b29..8c9a59702 100644 --- a/internal/oidc/login/post_login_handler.go +++ b/internal/oidc/login/post_login_handler.go @@ -12,13 +12,14 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/downstreamsession" + "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" ) -func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc { +func NewPostHandler(issuerURL string, upstreamIDPs provider.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { // Note that the login handler prevents this handler from being called with OIDC upstreams. - _, ldapUpstream, idpType, err := oidc.FindUpstreamIDPByNameAndType(upstreamIDPs, decodedState.UpstreamName, decodedState.UpstreamType) + _, ldapUpstream, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName) if err != nil { // This shouldn't normally happen because the authorization endpoint ensured that this provider existed // at that time. It would be possible in the unlikely event that the provider was deleted during the login. @@ -51,20 +52,20 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider downstreamsession.AutoApproveScopes(authorizeRequester) // Get the username and password form params from the POST body. - username := r.PostFormValue(usernameParamName) - password := r.PostFormValue(passwordParamName) + submittedUsername := r.PostFormValue(usernameParamName) + submittedPassword := r.PostFormValue(passwordParamName) // Treat blank username or password as a bad username/password combination, as opposed to an internal error. - if username == "" || password == "" { + if submittedUsername == "" || submittedPassword == "" { // User forgot to enter one of the required fields. // The user may try to log in again if they'd like, so redirect back to the login page with an error. return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr) } // Attempt to authenticate the user with the upstream IDP. - authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes()) + authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser(r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes()) if err != nil { - plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) + plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.Provider.GetName()) // There was some problem during authentication with the upstream, aside from bad username/password. // The user may try to log in again if they'd like, so redirect back to the login page with an error. return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowInternalError) @@ -79,10 +80,19 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider // Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps. // Both success and error responses from this point onwards should look like the usual fosite redirect // responses, and a happy redirect response will include a downstream authcode. - subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) - username = authenticateResponse.User.GetName() - groups := authenticateResponse.User.GetGroups() - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) + subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream.Provider, authenticateResponse) + upstreamUsername := authenticateResponse.User.GetName() + upstreamGroups := authenticateResponse.User.GetGroups() + + username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), ldapUpstream.Transforms, upstreamUsername, upstreamGroups) + if err != nil { + oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, + fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), false, + ) + return nil + } + + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{}) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 71bc914d5..1367f35b3 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package oidc contains common OIDC functionality needed by Pinniped. @@ -7,7 +7,6 @@ package oidc import ( "context" "crypto/subtle" - "errors" "fmt" "net/http" "net/url" @@ -18,12 +17,10 @@ import ( "github.com/ory/fosite/compose" errorsx "github.com/pkg/errors" - "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" @@ -279,24 +276,6 @@ func FositeErrorForLog(err error) []interface{} { return keysAndValues } -type UpstreamOIDCIdentityProvidersLister interface { - GetOIDCIdentityProviders() []provider.UpstreamOIDCIdentityProviderI -} - -type UpstreamLDAPIdentityProvidersLister interface { - GetLDAPIdentityProviders() []provider.UpstreamLDAPIdentityProviderI -} - -type UpstreamActiveDirectoryIdentityProviderLister interface { - GetActiveDirectoryIdentityProviders() []provider.UpstreamLDAPIdentityProviderI -} - -type UpstreamIdentityProvidersLister interface { - UpstreamOIDCIdentityProvidersLister - UpstreamLDAPIdentityProvidersLister - UpstreamActiveDirectoryIdentityProviderLister -} - func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) { if ScopeWasRequested(authorizeRequester, scopeName) { authorizeRequester.GrantScope(scopeName) @@ -377,41 +356,6 @@ func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken. return nil } -// FindUpstreamIDPByNameAndType finds the requested IDP by name and type, or returns an error. -// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. -func FindUpstreamIDPByNameAndType( - idpLister UpstreamIdentityProvidersLister, - upstreamName string, - upstreamType string, -) ( - provider.UpstreamOIDCIdentityProviderI, - provider.UpstreamLDAPIdentityProviderI, - psession.ProviderType, - error, -) { - switch upstreamType { - case string(v1alpha1.IDPTypeOIDC): - for _, p := range idpLister.GetOIDCIdentityProviders() { - if p.GetName() == upstreamName { - return p, nil, psession.ProviderTypeOIDC, nil - } - } - case string(v1alpha1.IDPTypeLDAP): - for _, p := range idpLister.GetLDAPIdentityProviders() { - if p.GetName() == upstreamName { - return nil, p, psession.ProviderTypeLDAP, nil - } - } - case string(v1alpha1.IDPTypeActiveDirectory): - for _, p := range idpLister.GetActiveDirectoryIdentityProviders() { - if p.GetName() == upstreamName { - return nil, p, psession.ProviderTypeActiveDirectory, nil - } - } - } - return nil, nil, "", errors.New("provider not found") -} - // WriteAuthorizeError writes an authorization error as it should be returned by the authorization endpoint and other // similar endpoints that are the end of the downstream authcode flow. Errors responses are written in the usual fosite style. func WriteAuthorizeError(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) { diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index ca26451e1..e9a4333af 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -4,182 +4,67 @@ package provider import ( - "context" "fmt" - "net/url" "sync" - "golang.org/x/oauth2" - "k8s.io/apimachinery/pkg/types" - - "go.pinniped.dev/internal/authenticators" - "go.pinniped.dev/pkg/oidcclient/nonce" - "go.pinniped.dev/pkg/oidcclient/oidctypes" - "go.pinniped.dev/pkg/oidcclient/pkce" -) - -type RevocableTokenType string - -// These strings correspond to the token types defined by https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 -const ( - RefreshTokenType RevocableTokenType = "refresh_token" - AccessTokenType RevocableTokenType = "access_token" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" ) -type UpstreamOIDCIdentityProviderI interface { - // GetName returns a name for this upstream provider, which will be used as a component of the path for the - // callback endpoint hosted by the Supervisor. - GetName() string - - // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. - GetClientID() string - - // GetResourceUID returns the Kubernetes resource ID - GetResourceUID() types.UID - - // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. - GetAuthorizationURL() *url.URL - - // HasUserInfoURL returns whether there is a non-empty value for userinfo_endpoint fetched from discovery. - HasUserInfoURL() bool - - // GetScopes returns the scopes to request in authorization (authcode or password grant) flow. - GetScopes() []string - - // GetUsernameClaim returns the ID Token username claim name. May return empty string, in which case we - // will use some reasonable defaults. - GetUsernameClaim() string - - // GetGroupsClaim returns the ID Token groups claim name. May return empty string, in which case we won't - // try to read groups from the upstream provider. - GetGroupsClaim() string - - // AllowsPasswordGrant returns true if a client should be allowed to use the resource owner password credentials grant - // flow with this upstream provider. When false, it should not be allowed. - AllowsPasswordGrant() bool - - // GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests. - GetAdditionalAuthcodeParams() map[string]string - - // GetAdditionalClaimMappings returns additional claims to be mapped from the upstream ID token. - GetAdditionalClaimMappings() map[string]string - - // PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and - // token validation. Returns the validated raw tokens as well as the parsed claims of the ID token. - PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) - - // ExchangeAuthcodeAndValidateTokens performs upstream OIDC authorization code exchange and token validation. - // Returns the validated raw tokens as well as the parsed claims of the ID token. - ExchangeAuthcodeAndValidateTokens( - ctx context.Context, - authcode string, - pkceCodeVerifier pkce.Code, - expectedIDTokenNonce nonce.Nonce, - redirectURI string, - ) (*oidctypes.Token, error) - - // PerformRefresh will call the provider's token endpoint to perform a refresh grant. The provider may or may not - // return a new ID or refresh token in the response. If it returns an ID token, then use ValidateToken to - // validate the ID token. - PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) - - // RevokeToken will attempt to revoke the given token, if the provider has a revocation endpoint. - // It may return an error wrapped by a RetryableRevocationError, which is an error indicating that it may - // be worth trying to revoke the same token again later. Any other error returned should be assumed to - // represent an error such that it is not worth retrying revocation later, even though revocation failed. - RevokeToken(ctx context.Context, token string, tokenType RevocableTokenType) error - - // ValidateTokenAndMergeWithUserInfo will validate the ID token. It will also merge the claims from the userinfo endpoint response - // into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated - // tokens, or an error. - ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) -} - -type UpstreamLDAPIdentityProviderI interface { - // GetName returns a name for this upstream provider. - GetName() string - - // GetURL returns a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". - // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user - // identifier by being combined with the user's UID, since user UIDs are only unique within one provider. - GetURL() *url.URL - - // GetResourceUID returns the Kubernetes resource ID - GetResourceUID() types.UID - - // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. - authenticators.UserAuthenticator - - // PerformRefresh performs a refresh against the upstream LDAP identity provider - PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes) (groups []string, err error) -} - -// RefreshAttributes contains information about the user from the original login request -// and previous refreshes. -type RefreshAttributes struct { - Username string - Subject string - DN string - Groups []string - AdditionalAttributes map[string]string - GrantedScopes []string -} - type DynamicUpstreamIDPProvider interface { - SetOIDCIdentityProviders(oidcIDPs []UpstreamOIDCIdentityProviderI) - GetOIDCIdentityProviders() []UpstreamOIDCIdentityProviderI - SetLDAPIdentityProviders(ldapIDPs []UpstreamLDAPIdentityProviderI) - GetLDAPIdentityProviders() []UpstreamLDAPIdentityProviderI - SetActiveDirectoryIdentityProviders(adIDPs []UpstreamLDAPIdentityProviderI) - GetActiveDirectoryIdentityProviders() []UpstreamLDAPIdentityProviderI + SetOIDCIdentityProviders(oidcIDPs []upstreamprovider.UpstreamOIDCIdentityProviderI) + GetOIDCIdentityProviders() []upstreamprovider.UpstreamOIDCIdentityProviderI + SetLDAPIdentityProviders(ldapIDPs []upstreamprovider.UpstreamLDAPIdentityProviderI) + GetLDAPIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI + SetActiveDirectoryIdentityProviders(adIDPs []upstreamprovider.UpstreamLDAPIdentityProviderI) + GetActiveDirectoryIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI } type dynamicUpstreamIDPProvider struct { - oidcUpstreams []UpstreamOIDCIdentityProviderI - ldapUpstreams []UpstreamLDAPIdentityProviderI - activeDirectoryUpstreams []UpstreamLDAPIdentityProviderI + oidcUpstreams []upstreamprovider.UpstreamOIDCIdentityProviderI + ldapUpstreams []upstreamprovider.UpstreamLDAPIdentityProviderI + activeDirectoryUpstreams []upstreamprovider.UpstreamLDAPIdentityProviderI mutex sync.RWMutex } func NewDynamicUpstreamIDPProvider() DynamicUpstreamIDPProvider { return &dynamicUpstreamIDPProvider{ - oidcUpstreams: []UpstreamOIDCIdentityProviderI{}, - ldapUpstreams: []UpstreamLDAPIdentityProviderI{}, - activeDirectoryUpstreams: []UpstreamLDAPIdentityProviderI{}, + oidcUpstreams: []upstreamprovider.UpstreamOIDCIdentityProviderI{}, + ldapUpstreams: []upstreamprovider.UpstreamLDAPIdentityProviderI{}, + activeDirectoryUpstreams: []upstreamprovider.UpstreamLDAPIdentityProviderI{}, } } -func (p *dynamicUpstreamIDPProvider) SetOIDCIdentityProviders(oidcIDPs []UpstreamOIDCIdentityProviderI) { +func (p *dynamicUpstreamIDPProvider) SetOIDCIdentityProviders(oidcIDPs []upstreamprovider.UpstreamOIDCIdentityProviderI) { p.mutex.Lock() // acquire a write lock defer p.mutex.Unlock() p.oidcUpstreams = oidcIDPs } -func (p *dynamicUpstreamIDPProvider) GetOIDCIdentityProviders() []UpstreamOIDCIdentityProviderI { +func (p *dynamicUpstreamIDPProvider) GetOIDCIdentityProviders() []upstreamprovider.UpstreamOIDCIdentityProviderI { p.mutex.RLock() // acquire a read lock defer p.mutex.RUnlock() return p.oidcUpstreams } -func (p *dynamicUpstreamIDPProvider) SetLDAPIdentityProviders(ldapIDPs []UpstreamLDAPIdentityProviderI) { +func (p *dynamicUpstreamIDPProvider) SetLDAPIdentityProviders(ldapIDPs []upstreamprovider.UpstreamLDAPIdentityProviderI) { p.mutex.Lock() // acquire a write lock defer p.mutex.Unlock() p.ldapUpstreams = ldapIDPs } -func (p *dynamicUpstreamIDPProvider) GetLDAPIdentityProviders() []UpstreamLDAPIdentityProviderI { +func (p *dynamicUpstreamIDPProvider) GetLDAPIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI { p.mutex.RLock() // acquire a read lock defer p.mutex.RUnlock() return p.ldapUpstreams } -func (p *dynamicUpstreamIDPProvider) SetActiveDirectoryIdentityProviders(adIDPs []UpstreamLDAPIdentityProviderI) { +func (p *dynamicUpstreamIDPProvider) SetActiveDirectoryIdentityProviders(adIDPs []upstreamprovider.UpstreamLDAPIdentityProviderI) { p.mutex.Lock() // acquire a write lock defer p.mutex.Unlock() p.activeDirectoryUpstreams = adIDPs } -func (p *dynamicUpstreamIDPProvider) GetActiveDirectoryIdentityProviders() []UpstreamLDAPIdentityProviderI { +func (p *dynamicUpstreamIDPProvider) GetActiveDirectoryIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI { p.mutex.RLock() // acquire a read lock defer p.mutex.RUnlock() return p.activeDirectoryUpstreams diff --git a/internal/oidc/provider/federation_domain_identity_providers_lister.go b/internal/oidc/provider/federation_domain_identity_providers_lister.go new file mode 100644 index 000000000..ce333d3ca --- /dev/null +++ b/internal/oidc/provider/federation_domain_identity_providers_lister.go @@ -0,0 +1,232 @@ +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/oidc/idplister" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/psession" +) + +// FederationDomainIdentityProvider represents an identity provider as configured in a FederationDomain's spec. +// All the fields are required and must be non-zero values. Note that this might be a reference to an IDP +// which is not currently loaded into the cache of available IDPs, e.g. due to the IDP's CR having validation errors. +type FederationDomainIdentityProvider struct { + DisplayName string + UID types.UID + Transforms *idtransform.TransformationPipeline +} + +// FederationDomainResolvedOIDCIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamOIDCIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedOIDCIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamOIDCIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +// FederationDomainResolvedLDAPIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamLDAPIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedLDAPIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamLDAPIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +type FederationDomainIdentityProvidersFinderI interface { + FindDefaultIDP() ( + *FederationDomainResolvedOIDCIdentityProvider, + *FederationDomainResolvedLDAPIdentityProvider, + error, + ) + + FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( + *FederationDomainResolvedOIDCIdentityProvider, + *FederationDomainResolvedLDAPIdentityProvider, + error, + ) +} + +type FederationDomainIdentityProvidersListerI interface { + GetOIDCIdentityProviders() []*FederationDomainResolvedOIDCIdentityProvider + GetLDAPIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider + GetActiveDirectoryIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider +} + +// FederationDomainIdentityProvidersLister wraps an UpstreamIdentityProvidersLister. The lister which is being +// wrapped should contain all valid upstream providers that are currently defined in the Supervisor. +// FederationDomainIdentityProvidersLister provides a lookup method which only looks up IDPs within those which +// have allowed resource IDs, and also uses display names (name aliases) instead of the actual resource names to do the +// lookups. It also provides list methods which only list the allowed identity providers (to be used by the IDP +// discovery endpoint, for example). +type FederationDomainIdentityProvidersLister struct { + wrappedLister idplister.UpstreamIdentityProvidersLister + configuredIdentityProviders []*FederationDomainIdentityProvider + defaultIdentityProvider *FederationDomainIdentityProvider + idpDisplayNamesToResourceUIDsMap map[string]types.UID + allowedIDPResourceUIDs sets.Set[types.UID] +} + +// NewFederationDomainUpstreamIdentityProvidersLister returns a new FederationDomainIdentityProvidersLister +// which only lists those IDPs allowed by its parameter. Every FederationDomainIdentityProvider in the +// federationDomainIssuer parameter's IdentityProviders() list must have a unique DisplayName. +// Note that a single underlying IDP UID may be used by multiple FederationDomainIdentityProvider in the parameter. +// The wrapped lister should contain all valid upstream providers that are defined in the Supervisor, and is expected to +// be thread-safe and to change its contents over time. The FederationDomainIdentityProvidersLister will filter out the +// ones that don't apply to this federation domain. +func NewFederationDomainUpstreamIdentityProvidersLister( + federationDomainIssuer *FederationDomainIssuer, + wrappedLister idplister.UpstreamIdentityProvidersLister, +) *FederationDomainIdentityProvidersLister { + // Create a copy of the input slice so we won't need to worry about the caller accidentally changing it. + copyOfFederationDomainIdentityProviders := []*FederationDomainIdentityProvider{} + // Create a map and a set for quick lookups of the same data that was passed in via the + // federationDomainIssuer parameter. + allowedResourceUIDs := sets.New[types.UID]() + idpDisplayNamesToResourceUIDsMap := map[string]types.UID{} + for _, idp := range federationDomainIssuer.IdentityProviders() { + allowedResourceUIDs.Insert(idp.UID) + idpDisplayNamesToResourceUIDsMap[idp.DisplayName] = idp.UID + shallowCopyOfIDP := *idp + copyOfFederationDomainIdentityProviders = append(copyOfFederationDomainIdentityProviders, &shallowCopyOfIDP) + } + + return &FederationDomainIdentityProvidersLister{ + wrappedLister: wrappedLister, + configuredIdentityProviders: copyOfFederationDomainIdentityProviders, + defaultIdentityProvider: federationDomainIssuer.DefaultIdentityProvider(), + idpDisplayNamesToResourceUIDsMap: idpDisplayNamesToResourceUIDsMap, + allowedIDPResourceUIDs: allowedResourceUIDs, + } +} + +// FindUpstreamIDPByDisplayName selects either an OIDC, LDAP, or ActiveDirectory IDP, or returns an error. +// It only considers the allowed IDPs while doing the lookup by display name. +// Note that ActiveDirectory and LDAP IDPs both return the same type, but with different SessionProviderType values. +func (u *FederationDomainIdentityProvidersLister) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( + *FederationDomainResolvedOIDCIdentityProvider, + *FederationDomainResolvedLDAPIdentityProvider, + error, +) { + // Given a display name, look up the identity provider's UID for that display name. + idpUIDForDisplayName, ok := u.idpDisplayNamesToResourceUIDsMap[upstreamIDPDisplayName] + if !ok { + return nil, nil, fmt.Errorf("identity provider not found: %q", upstreamIDPDisplayName) + } + // Find the IDP with that UID. It could be any type, so look at all types to find it. + for _, p := range u.GetOIDCIdentityProviders() { + if p.Provider.GetResourceUID() == idpUIDForDisplayName { + return p, nil, nil + } + } + for _, p := range u.GetLDAPIdentityProviders() { + if p.Provider.GetResourceUID() == idpUIDForDisplayName { + return nil, p, nil + } + } + for _, p := range u.GetActiveDirectoryIdentityProviders() { + if p.Provider.GetResourceUID() == idpUIDForDisplayName { + return nil, p, nil + } + } + return nil, nil, fmt.Errorf("identity provider not found: %q", upstreamIDPDisplayName) +} + +// FindDefaultIDP works like FindUpstreamIDPByDisplayName, but finds the default IDP instead of finding by name. +// If there is no default IDP for this federation domain, then FindDefaultIDP will return an error. +// This can be used to handle the backwards compatibility mode where an authorization request could be made +// without specifying an IDP name, and there are no IDPs explicitly specified on the FederationDomain, and there +// is exactly one IDP CR defined in the Supervisor namespace. +func (u *FederationDomainIdentityProvidersLister) FindDefaultIDP() ( + *FederationDomainResolvedOIDCIdentityProvider, + *FederationDomainResolvedLDAPIdentityProvider, + error, +) { + if u.defaultIdentityProvider == nil { + return nil, nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") + } + return u.FindUpstreamIDPByDisplayName(u.defaultIdentityProvider.DisplayName) +} + +// GetOIDCIdentityProviders lists only the OIDC providers for this FederationDomain. +func (u *FederationDomainIdentityProvidersLister) GetOIDCIdentityProviders() []*FederationDomainResolvedOIDCIdentityProvider { + // Get the cached providers once at the start in case they change during the rest of this function. + cachedProviders := u.wrappedLister.GetOIDCIdentityProviders() + providers := []*FederationDomainResolvedOIDCIdentityProvider{} + // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might + // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... + for _, idp := range u.configuredIdentityProviders { + // Check if the IDP used by that displayName is in the cached available OIDC providers. + for _, p := range cachedProviders { + if idp.UID == p.GetResourceUID() { + // Found it, so append it to the result. + providers = append(providers, &FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: idp.DisplayName, + Provider: p, + SessionProviderType: psession.ProviderTypeOIDC, + Transforms: idp.Transforms, + }) + } + } + } + return providers +} + +// GetLDAPIdentityProviders lists only the LDAP providers for this FederationDomain. +func (u *FederationDomainIdentityProvidersLister) GetLDAPIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider { + // Get the cached providers once at the start in case they change during the rest of this function. + cachedProviders := u.wrappedLister.GetLDAPIdentityProviders() + providers := []*FederationDomainResolvedLDAPIdentityProvider{} + // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might + // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... + for _, idp := range u.configuredIdentityProviders { + // Check if the IDP used by that displayName is in the cached available LDAP providers. + for _, p := range cachedProviders { + if idp.UID == p.GetResourceUID() { + // Found it, so append it to the result. + providers = append(providers, &FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: idp.DisplayName, + Provider: p, + SessionProviderType: psession.ProviderTypeLDAP, + Transforms: idp.Transforms, + }) + } + } + } + return providers +} + +// GetActiveDirectoryIdentityProviders lists only the ActiveDirectory providers for this FederationDomain. +func (u *FederationDomainIdentityProvidersLister) GetActiveDirectoryIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider { + // Get the cached providers once at the start in case they change during the rest of this function. + cachedProviders := u.wrappedLister.GetActiveDirectoryIdentityProviders() + providers := []*FederationDomainResolvedLDAPIdentityProvider{} + // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might + // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... + for _, idp := range u.configuredIdentityProviders { + // Check if the IDP used by that displayName is in the cached available ActiveDirectory providers. + for _, p := range cachedProviders { + if idp.UID == p.GetResourceUID() { + // Found it, so append it to the result. + providers = append(providers, &FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: idp.DisplayName, + Provider: p, + SessionProviderType: psession.ProviderTypeActiveDirectory, + Transforms: idp.Transforms, + }) + } + } + } + return providers +} diff --git a/internal/oidc/provider/federation_domain_issuer.go b/internal/oidc/provider/federation_domain_issuer.go index 29e3683cd..bdb27f142 100644 --- a/internal/oidc/provider/federation_domain_issuer.go +++ b/internal/oidc/provider/federation_domain_issuer.go @@ -17,20 +17,42 @@ type FederationDomainIssuer struct { issuer string issuerHost string issuerPath string + + // identityProviders should be used when they are explicitly specified in the FederationDomain's spec. + identityProviders []*FederationDomainIdentityProvider + // defaultIdentityProvider should be used only for the backwards compatibility mode where identity providers + // are not explicitly specified in the FederationDomain's spec, and there is exactly one IDP CR defined in the + // Supervisor's namespace. + defaultIdentityProvider *FederationDomainIdentityProvider } // NewFederationDomainIssuer returns a FederationDomainIssuer. // Performs validation, and returns any error from validation. -func NewFederationDomainIssuer(issuer string) (*FederationDomainIssuer, error) { - p := FederationDomainIssuer{issuer: issuer} - err := p.validate() +func NewFederationDomainIssuer( + issuer string, + identityProviders []*FederationDomainIdentityProvider, +) (*FederationDomainIssuer, error) { + p := FederationDomainIssuer{issuer: issuer, identityProviders: identityProviders} + err := p.validateURL() if err != nil { return nil, err } return &p, nil } -func (p *FederationDomainIssuer) validate() error { +func NewFederationDomainIssuerWithDefaultIDP( + issuer string, + defaultIdentityProvider *FederationDomainIdentityProvider, +) (*FederationDomainIssuer, error) { + fdi, err := NewFederationDomainIssuer(issuer, []*FederationDomainIdentityProvider{defaultIdentityProvider}) + if err != nil { + return nil, err + } + fdi.defaultIdentityProvider = defaultIdentityProvider + return fdi, nil +} + +func (p *FederationDomainIssuer) validateURL() error { if p.issuer == "" { return constable.Error("federation domain must have an issuer") } @@ -84,3 +106,13 @@ func (p *FederationDomainIssuer) IssuerHost() string { func (p *FederationDomainIssuer) IssuerPath() string { return p.issuerPath } + +// IdentityProviders returns the IdentityProviders. +func (p *FederationDomainIssuer) IdentityProviders() []*FederationDomainIdentityProvider { + return p.identityProviders +} + +// DefaultIdentityProvider will return nil when there is no default. +func (p *FederationDomainIssuer) DefaultIdentityProvider() *FederationDomainIdentityProvider { + return p.defaultIdentityProvider +} diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 0cd3e3e34..5793765d7 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -19,6 +19,7 @@ import ( "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/dynamiccodec" "go.pinniped.dev/internal/oidc/idpdiscovery" + "go.pinniped.dev/internal/oidc/idplister" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/login" "go.pinniped.dev/internal/oidc/oidcclientvalidator" @@ -36,11 +37,11 @@ import ( type Manager struct { mu sync.RWMutex providers []*provider.FederationDomainIssuer - providerHandlers map[string]http.Handler // map of all routes for all providers - nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request - dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data - upstreamIDPs oidc.UpstreamIdentityProvidersLister // in-memory cache of upstream IDPs - secretCache *secret.Cache // in-memory cache of cryptographic material + providerHandlers map[string]http.Handler // map of all routes for all providers + nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request + dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data + upstreamIDPs idplister.UpstreamIdentityProvidersLister // in-memory cache of upstream IDPs + secretCache *secret.Cache // in-memory cache of cryptographic material secretsClient corev1client.SecretInterface oidcClientsClient v1alpha1.OIDCClientInterface } @@ -52,7 +53,7 @@ type Manager struct { func NewManager( nextHandler http.Handler, dynamicJWKSProvider jwks.DynamicJWKSProvider, - upstreamIDPs oidc.UpstreamIdentityProvidersLister, + upstreamIDPs idplister.UpstreamIdentityProvidersLister, secretCache *secret.Cache, secretsClient corev1client.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface, @@ -83,17 +84,17 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs m.providers = federationDomains m.providerHandlers = make(map[string]http.Handler) - var csrfCookieEncoder = dynamiccodec.New( + csrfCookieEncoder := dynamiccodec.New( oidc.CSRFCookieLifespan, m.secretCache.GetCSRFCookieEncoderHashKey, func() []byte { return nil }, ) - for _, incomingProvider := range federationDomains { - issuer := incomingProvider.Issuer() - issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath() + for _, incomingFederationDomain := range federationDomains { + issuerURL := incomingFederationDomain.Issuer() + issuerHostWithPath := strings.ToLower(incomingFederationDomain.IssuerHost()) + "/" + incomingFederationDomain.IssuerPath() - tokenHMACKeyGetter := wrapGetter(incomingProvider.Issuer(), m.secretCache.GetTokenHMACKey) + tokenHMACKeyGetter := wrapGetter(incomingFederationDomain.Issuer(), m.secretCache.GetTokenHMACKey) timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() @@ -101,7 +102,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs // the upstream callback endpoint is called later. oauthHelperWithNullStorage := oidc.FositeOauth2Helper( oidc.NewNullStorage(m.secretsClient, m.oidcClientsClient, oidcclientvalidator.DefaultMinBcryptCost), - issuer, + issuerURL, tokenHMACKeyGetter, nil, timeoutsConfiguration, @@ -110,27 +111,29 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs // For all the other endpoints, make another oauth helper with exactly the same settings except use real storage. oauthHelperWithKubeStorage := oidc.FositeOauth2Helper( oidc.NewKubeStorage(m.secretsClient, m.oidcClientsClient, timeoutsConfiguration, oidcclientvalidator.DefaultMinBcryptCost), - issuer, + issuerURL, tokenHMACKeyGetter, m.dynamicJWKSProvider, timeoutsConfiguration, ) - var upstreamStateEncoder = dynamiccodec.New( + upstreamStateEncoder := dynamiccodec.New( timeoutsConfiguration.UpstreamStateParamLifespan, - wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderHashKey), - wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey), + wrapGetter(incomingFederationDomain.Issuer(), m.secretCache.GetStateEncoderHashKey), + wrapGetter(incomingFederationDomain.Issuer(), m.secretCache.GetStateEncoderBlockKey), ) - m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) + idpLister := provider.NewFederationDomainUpstreamIdentityProvidersLister(incomingFederationDomain, m.upstreamIDPs) - m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuer, m.dynamicJWKSProvider) + m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuerURL) - m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPathV1Alpha1)] = idpdiscovery.NewHandler(m.upstreamIDPs) + m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuerURL, m.dynamicJWKSProvider) + + m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPathV1Alpha1)] = idpdiscovery.NewHandler(idpLister) m.providerHandlers[(issuerHostWithPath + oidc.AuthorizationEndpointPath)] = auth.NewHandler( - issuer, - m.upstreamIDPs, + issuerURL, + idpLister, oauthHelperWithNullStorage, oauthHelperWithKubeStorage, csrftoken.Generate, @@ -141,26 +144,26 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs ) m.providerHandlers[(issuerHostWithPath + oidc.CallbackEndpointPath)] = callback.NewHandler( - m.upstreamIDPs, + idpLister, oauthHelperWithKubeStorage, upstreamStateEncoder, csrfCookieEncoder, - issuer+oidc.CallbackEndpointPath, + issuerURL+oidc.CallbackEndpointPath, ) m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler( - m.upstreamIDPs, + idpLister, oauthHelperWithKubeStorage, ) m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( upstreamStateEncoder, csrfCookieEncoder, - login.NewGetHandler(incomingProvider.IssuerPath()+oidc.PinnipedLoginPath), - login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage), + login.NewGetHandler(incomingFederationDomain.IssuerPath()+oidc.PinnipedLoginPath), + login.NewPostHandler(issuerURL, idpLister, oauthHelperWithKubeStorage), ) - plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer) + plog.Debug("oidc provider manager added or updated issuer", "issuer", issuerURL) } } diff --git a/internal/oidc/provider/upstreamprovider/upsteam_provider.go b/internal/oidc/provider/upstreamprovider/upsteam_provider.go new file mode 100644 index 000000000..68695b254 --- /dev/null +++ b/internal/oidc/provider/upstreamprovider/upsteam_provider.go @@ -0,0 +1,126 @@ +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamprovider + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + "k8s.io/apimachinery/pkg/types" + + "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/oidctypes" + "go.pinniped.dev/pkg/oidcclient/pkce" +) + +type RevocableTokenType string + +// These strings correspond to the token types defined by https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 +const ( + RefreshTokenType RevocableTokenType = "refresh_token" + AccessTokenType RevocableTokenType = "access_token" +) + +// RefreshAttributes contains information about the user from the original login request +// and previous refreshes. +type RefreshAttributes struct { + Username string + Subject string + DN string + Groups []string + AdditionalAttributes map[string]string + GrantedScopes []string +} + +type UpstreamOIDCIdentityProviderI interface { + // GetName returns a name for this upstream provider. The controller watching the OIDCIdentityProviders will + // set this to be the Name of the CR from its metadata. Note that this is different from the DisplayName configured + // in each FederationDomain that uses this provider, so this name is for internal use only, not for interacting + // with clients. Clients should not expect to see this name or send this name. + GetName() string + + // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. + GetClientID() string + + // GetResourceUID returns the Kubernetes resource ID + GetResourceUID() types.UID + + // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. + GetAuthorizationURL() *url.URL + + // HasUserInfoURL returns whether there is a non-empty value for userinfo_endpoint fetched from discovery. + HasUserInfoURL() bool + + // GetScopes returns the scopes to request in authorization (authcode or password grant) flow. + GetScopes() []string + + // GetUsernameClaim returns the ID Token username claim name. May return empty string, in which case we + // will use some reasonable defaults. + GetUsernameClaim() string + + // GetGroupsClaim returns the ID Token groups claim name. May return empty string, in which case we won't + // try to read groups from the upstream provider. + GetGroupsClaim() string + + // AllowsPasswordGrant returns true if a client should be allowed to use the resource owner password credentials grant + // flow with this upstream provider. When false, it should not be allowed. + AllowsPasswordGrant() bool + + // GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests. + GetAdditionalAuthcodeParams() map[string]string + + // GetAdditionalClaimMappings returns additional claims to be mapped from the upstream ID token. + GetAdditionalClaimMappings() map[string]string + + // PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and + // token validation. Returns the validated raw tokens as well as the parsed claims of the ID token. + PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) + + // ExchangeAuthcodeAndValidateTokens performs upstream OIDC authorization code exchange and token validation. + // Returns the validated raw tokens as well as the parsed claims of the ID token. + ExchangeAuthcodeAndValidateTokens( + ctx context.Context, + authcode string, + pkceCodeVerifier pkce.Code, + expectedIDTokenNonce nonce.Nonce, + redirectURI string, + ) (*oidctypes.Token, error) + + // PerformRefresh will call the provider's token endpoint to perform a refresh grant. The provider may or may not + // return a new ID or refresh token in the response. If it returns an ID token, then use ValidateToken to + // validate the ID token. + PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) + + // RevokeToken will attempt to revoke the given token, if the provider has a revocation endpoint. + // It may return an error wrapped by a RetryableRevocationError, which is an error indicating that it may + // be worth trying to revoke the same token again later. Any other error returned should be assumed to + // represent an error such that it is not worth retrying revocation later, even though revocation failed. + RevokeToken(ctx context.Context, token string, tokenType RevocableTokenType) error + + // ValidateTokenAndMergeWithUserInfo will validate the ID token. It will also merge the claims from the userinfo endpoint response + // into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated + // tokens, or an error. + ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) +} + +type UpstreamLDAPIdentityProviderI interface { + // GetName returns a name for this upstream provider. + GetName() string + + // GetURL returns a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". + // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user + // identifier by being combined with the user's UID, since user UIDs are only unique within one provider. + GetURL() *url.URL + + // GetResourceUID returns the Kubernetes resource ID + GetResourceUID() types.UID + + // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. + authenticators.UserAuthenticator + + // PerformRefresh performs a refresh against the upstream LDAP identity provider + PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes) (groups []string, err error) +} diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index 9e016ee29..4d2bf7081 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package token provides a handler for the OIDC token endpoint. @@ -19,15 +19,17 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) func NewHandler( - idpLister oidc.UpstreamIdentityProvidersLister, + idpLister provider.FederationDomainIdentityProvidersListerI, oauthHelper fosite.OAuth2Provider, ) http.Handler { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { @@ -95,7 +97,7 @@ func errUpstreamRefreshError() *fosite.RFC6749Error { } } -func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error { +func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, idpLister provider.FederationDomainIdentityProvidersListerI) error { session := accessRequest.GetSession().(*psession.PinnipedSession) customSessionData := session.Custom @@ -113,11 +115,11 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, switch customSessionData.ProviderType { case psession.ProviderTypeOIDC: - return upstreamOIDCRefresh(ctx, session, providerCache, grantedScopes, clientID) + return upstreamOIDCRefresh(ctx, session, idpLister, grantedScopes, clientID) case psession.ProviderTypeLDAP: - return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes, clientID) + return upstreamLDAPRefresh(ctx, idpLister, session, grantedScopes, clientID) case psession.ProviderTypeActiveDirectory: - return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes, clientID) + return upstreamLDAPRefresh(ctx, idpLister, session, grantedScopes, clientID) default: return errorsx.WithStack(errMissingUpstreamSessionInternalError()) } @@ -126,7 +128,7 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, func upstreamOIDCRefresh( ctx context.Context, session *psession.PinnipedSession, - providerCache oidc.UpstreamIdentityProvidersLister, + idpLister provider.FederationDomainIdentityProvidersListerI, grantedScopes []string, clientID string, ) error { @@ -143,7 +145,7 @@ func upstreamOIDCRefresh( return errorsx.WithStack(errMissingUpstreamSessionInternalError()) } - p, err := findOIDCProviderByNameAndValidateUID(s, providerCache) + p, err := findOIDCProviderByNameAndValidateUID(s, idpLister) if err != nil { return err } @@ -153,7 +155,7 @@ func upstreamOIDCRefresh( var tokens *oauth2.Token if refreshTokenStored { - tokens, err = p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken) + tokens, err = p.Provider.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken) if err != nil { return errUpstreamRefreshError().WithHint( "Upstream refresh failed.", @@ -174,7 +176,7 @@ func upstreamOIDCRefresh( // way to check that the user's session was not revoked on the server. // The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at // least some providers do not include one, so we skip the nonce validation here (but not other validations). - validatedTokens, err := p.ValidateTokenAndMergeWithUserInfo(ctx, tokens, "", hasIDTok, accessTokenStored) + validatedTokens, err := p.Provider.ValidateTokenAndMergeWithUserInfo(ctx, tokens, "", hasIDTok, accessTokenStored) if err != nil { return errUpstreamRefreshError().WithHintf( "Upstream refresh returned an invalid ID token or UserInfo response.").WithTrace(err). @@ -182,12 +184,22 @@ func upstreamOIDCRefresh( } mergedClaims := validatedTokens.IDToken.Claims + oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) + if err != nil { + return err + } + oldTransformedGroups, err := getDownstreamGroupsFromPinnipedSession(session) + if err != nil { + return err + } + // To the extent possible, check that the user's basic identity hasn't changed. - err = validateIdentityUnchangedSinceInitialLogin(mergedClaims, session, p.GetUsernameClaim()) + err = validateSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims, session) if err != nil { return err } + var refreshedUntransformedGroups []string groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups) if groupsScope { //nolint:nestif // If possible, update the user's group memberships. The configured groups claim name (if there is one) may or @@ -197,26 +209,44 @@ func upstreamOIDCRefresh( // If the claim is found, then use it to update the user's group membership in the session. // If the claim is not found, then we have no new information about groups, so skip updating the group membership // and let any old groups memberships in the session remain. - refreshedGroups, err := downstreamsession.GetGroupsFromUpstreamIDToken(p, mergedClaims) + refreshedUntransformedGroups, err = downstreamsession.GetGroupsFromUpstreamIDToken(p.Provider, mergedClaims) if err != nil { return errUpstreamRefreshError().WithHintf( "Upstream refresh error while extracting groups claim.").WithTrace(err). WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType) } - if refreshedGroups != nil { - oldGroups, err := getDownstreamGroupsFromPinnipedSession(session) - if err != nil { - return err - } - username, err := getDownstreamUsernameFromPinnipedSession(session) - if err != nil { - return err - } - warnIfGroupsChanged(ctx, oldGroups, refreshedGroups, username, clientID) - session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = refreshedGroups - } } + // It's possible that a username wasn't returned by the upstream provider during refresh, + // but if it is, verify that the transformed version of it hasn't changed. + refreshedUntransformedUsername, hasRefreshedUntransformedUsername := getString(mergedClaims, p.Provider.GetUsernameClaim()) + + if !hasRefreshedUntransformedUsername { + // If we could not get a new username, then we still need the untransformed username to be able to + // run the transformations again, so fetch the original untransformed username from the session. + refreshedUntransformedUsername = s.UpstreamUsername + } + if refreshedUntransformedGroups == nil { + // If we could not get a new list of groups, then we still need the untransformed groups list to be able to + // run the transformations again, so fetch the original untransformed groups list from the session. + refreshedUntransformedGroups = s.UpstreamGroups + } + + transformationResult, err := transformRefreshedIdentity(ctx, + p.Transforms, + oldTransformedUsername, + refreshedUntransformedUsername, + refreshedUntransformedGroups, + s.ProviderName, + s.ProviderType, + ) + if err != nil { + return err + } + + warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) + session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = refreshedUntransformedGroups + // Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in // the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding // overwriting the old one. @@ -238,7 +268,7 @@ func diffSortedGroups(oldGroups, newGroups []string) ([]string, []string) { return added.List(), removed.List() } -func validateIdentityUnchangedSinceInitialLogin(mergedClaims map[string]interface{}, session *psession.PinnipedSession, usernameClaimName string) error { +func validateSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims map[string]interface{}, session *psession.PinnipedSession) error { s := session.Custom // If we have any claims at all, we better have a subject, and it better match the previous value. @@ -260,19 +290,6 @@ func validateIdentityUnchangedSinceInitialLogin(mergedClaims map[string]interfac WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType) } - newUsername, hasUsername := getString(mergedClaims, usernameClaimName) - oldUsername, err := getDownstreamUsernameFromPinnipedSession(session) - if err != nil { - return err - } - // It's possible that a username wasn't returned by the upstream provider during refresh, - // but if it is, verify that it hasn't changed. - if hasUsername && oldUsername != newUsername { - return errUpstreamRefreshError().WithHintf( - "Upstream refresh failed.").WithTrace(errors.New("username in upstream refresh does not match previous value")). - WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType) - } - newIssuer, hasIssuer := getString(mergedClaims, oidcapi.IDTokenClaimIssuer) // It's possible that an issuer wasn't returned by the upstream provider during refresh, // but if it is, verify that it hasn't changed. @@ -292,11 +309,11 @@ func getString(m map[string]interface{}, key string) (string, bool) { func findOIDCProviderByNameAndValidateUID( s *psession.CustomSessionData, - providerCache oidc.UpstreamIdentityProvidersLister, -) (provider.UpstreamOIDCIdentityProviderI, error) { - for _, p := range providerCache.GetOIDCIdentityProviders() { - if p.GetName() == s.ProviderName { - if p.GetResourceUID() != s.ProviderUID { + idpLister provider.FederationDomainIdentityProvidersListerI, +) (*provider.FederationDomainResolvedOIDCIdentityProvider, error) { + for _, p := range idpLister.GetOIDCIdentityProviders() { + if p.Provider.GetName() == s.ProviderName { + if p.Provider.GetResourceUID() != s.ProviderUID { return nil, errorsx.WithStack(errUpstreamRefreshError().WithHint( "Provider from upstream session data has changed its resource UID since authentication.")) } @@ -310,27 +327,25 @@ func findOIDCProviderByNameAndValidateUID( func upstreamLDAPRefresh( ctx context.Context, - providerCache oidc.UpstreamIdentityProvidersLister, + idpLister provider.FederationDomainIdentityProvidersListerI, session *psession.PinnipedSession, grantedScopes []string, clientID string, ) error { - username, err := getDownstreamUsernameFromPinnipedSession(session) + oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) if err != nil { return err } subject := session.Fosite.Claims.Subject - var oldGroups []string + var oldTransformedGroups []string if slices.Contains(grantedScopes, oidcapi.ScopeGroups) { - oldGroups, err = getDownstreamGroupsFromPinnipedSession(session) + oldTransformedGroups, err = getDownstreamGroupsFromPinnipedSession(session) if err != nil { return err } } - s := session.Custom - // if you have neither a valid ldap session config nor a valid active directory session config validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != "" validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != "" if !(validLDAP || validAD) { @@ -344,20 +359,19 @@ func upstreamLDAPRefresh( additionalAttributes = s.ActiveDirectory.ExtraRefreshAttributes } - // get ldap/ad provider out of cache - p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache) + p, dn, err := findLDAPProviderByNameAndValidateUID(s, idpLister) if err != nil { return err } if session.IDTokenClaims().AuthTime.IsZero() { return errorsx.WithStack(errMissingUpstreamSessionInternalError()) } - // run PerformRefresh - groups, err := p.PerformRefresh(ctx, provider.RefreshAttributes{ - Username: username, + + refreshedUntransformedGroups, err := p.Provider.PerformRefresh(ctx, upstreamprovider.RefreshAttributes{ + Username: s.UpstreamUsername, Subject: subject, DN: dn, - Groups: oldGroups, + Groups: s.UpstreamGroups, AdditionalAttributes: additionalAttributes, GrantedScopes: grantedScopes, }) @@ -366,33 +380,79 @@ func upstreamLDAPRefresh( "Upstream refresh failed.").WithTrace(err). WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType) } + + transformationResult, err := transformRefreshedIdentity(ctx, + p.Transforms, + oldTransformedUsername, + s.UpstreamUsername, + refreshedUntransformedGroups, + s.ProviderName, + s.ProviderType, + ) + if err != nil { + return err + } + groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups) if groupsScope { - warnIfGroupsChanged(ctx, oldGroups, groups, username, clientID) + warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) // Replace the old value with the new value. - session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = groups + session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = transformationResult.Groups } return nil } +func transformRefreshedIdentity( + ctx context.Context, + transforms *idtransform.TransformationPipeline, + oldTransformedUsername string, + upstreamUsername string, + upstreamGroups []string, + providerName string, + providerType psession.ProviderType, +) (*idtransform.TransformationResult, error) { + transformationResult, err := transforms.Evaluate(ctx, upstreamUsername, upstreamGroups) + if err != nil { + return nil, errUpstreamRefreshError().WithHintf( + "Upstream refresh error while applying configured identity transformations."). + WithTrace(err). + WithDebugf("provider name: %q, provider type: %q", providerName, providerType) + } + + if !transformationResult.AuthenticationAllowed { + return nil, errUpstreamRefreshError().WithHintf( + "Upstream refresh rejected by configured identity policy: %s.", transformationResult.RejectedAuthenticationMessage). + WithDebugf("provider name: %q, provider type: %q", providerName, providerType) + } + + if oldTransformedUsername != transformationResult.Username { + return nil, errUpstreamRefreshError().WithHintf( + "Upstream refresh failed."). + WithTrace(errors.New("username in upstream refresh does not match previous value")). + WithDebugf("provider name: %q, provider type: %q", providerName, providerType) + } + + return transformationResult, nil +} + func findLDAPProviderByNameAndValidateUID( s *psession.CustomSessionData, - providerCache oidc.UpstreamIdentityProvidersLister, -) (provider.UpstreamLDAPIdentityProviderI, string, error) { - var providers []provider.UpstreamLDAPIdentityProviderI + idpLister provider.FederationDomainIdentityProvidersListerI, +) (*provider.FederationDomainResolvedLDAPIdentityProvider, string, error) { + var providers []*provider.FederationDomainResolvedLDAPIdentityProvider var dn string if s.ProviderType == psession.ProviderTypeLDAP { - providers = providerCache.GetLDAPIdentityProviders() + providers = idpLister.GetLDAPIdentityProviders() dn = s.LDAP.UserDN } else if s.ProviderType == psession.ProviderTypeActiveDirectory { - providers = providerCache.GetActiveDirectoryIdentityProviders() + providers = idpLister.GetActiveDirectoryIdentityProviders() dn = s.ActiveDirectory.UserDN } for _, p := range providers { - if p.GetName() == s.ProviderName { - if p.GetResourceUID() != s.ProviderUID { + if p.Provider.GetName() == s.ProviderName { + if p.Provider.GetResourceUID() != s.ProviderUID { return nil, "", errorsx.WithStack(errUpstreamRefreshError().WithHint( "Provider from upstream session data has changed its resource UID since authentication."). WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)) diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go index 136b2312c..bc995ba0f 100644 --- a/internal/psession/pinniped_session.go +++ b/internal/psession/pinniped_session.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package psession @@ -32,6 +32,18 @@ type CustomSessionData struct { // all users must have a username. Username string `json:"username"` + // UpstreamUsername is the username from the upstream identity provider during the user's initial login before + // identity transformations were applied. We store this so that we can still reapply identity transformations + // during refresh flows even when an upstream OIDC provider does not return the username again during the upstream + // refresh, and so we can validate that same untransformed username was found during an LDAP refresh. + UpstreamUsername string `json:"upstreamUsername"` + + // UpstreamGroups is the groups list from the upstream identity provider during the user's initial login before + // identity transformations were applied. We store this so that we can still reapply identity transformations + // during refresh flows even when an OIDC provider does not return the groups again during the upstream + // refresh, and when the LDAP search was configured to skip group refreshes. + UpstreamGroups []string `json:"upstreamGroups"` + // The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session. // This should be validated again upon downstream refresh to make sure that we are not refreshing against // a different identity provider CRD which just happens to have the same name. @@ -41,11 +53,12 @@ type CustomSessionData struct { // The Kubernetes resource name of the identity provider CRD for the upstream IDP used to start this session. // Used during a downstream refresh to decide which upstream to refresh. - // Also used to decide which of the pointer types below should be used. + // Also used by the session storage garbage collector to decide which upstream to use for token revocation. ProviderName string `json:"providerName"` // The type of the identity provider for the upstream IDP used to start this session. // Used during a downstream refresh to decide which upstream to refresh. + // Also used to decide which of the pointer types below should be used. ProviderType ProviderType `json:"providerType"` // Warnings that were encountered at some point during login that should be emitted to the client. @@ -55,8 +68,10 @@ type CustomSessionData struct { // Only used when ProviderType == "oidc". OIDC *OIDCSessionData `json:"oidc,omitempty"` + // Only used when ProviderType == "ldap". LDAP *LDAPSessionData `json:"ldap,omitempty"` + // Only used when ProviderType == "activedirectory". ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"` } diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index bfbb36bf9..11b5c9885 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -169,6 +169,9 @@ func prepareControllers( clock.RealClock{}, pinnipedClient, federationDomainInformer, + pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), controllerlib.WithInformer, ), singletonWorker, diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 474e5cbc6..ee566c1f7 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -35,6 +35,7 @@ import ( pkce2 "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -77,7 +78,7 @@ type PerformRefreshArgs struct { type RevokeTokenArgs struct { Ctx context.Context Token string - TokenType provider.RevocableTokenType + TokenType upstreamprovider.RevocableTokenType } // ValidateTokenAndMergeWithUserInfoArgs is used to spy on calls to @@ -93,7 +94,7 @@ type ValidateTokenAndMergeWithUserInfoArgs struct { type ValidateRefreshArgs struct { Ctx context.Context Tok *oauth2.Token - StoredAttributes provider.RefreshAttributes + StoredAttributes upstreamprovider.RefreshAttributes } type TestUpstreamLDAPIdentityProvider struct { @@ -107,7 +108,7 @@ type TestUpstreamLDAPIdentityProvider struct { PerformRefreshGroups []string } -var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} +var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID { return u.ResourceUID @@ -125,7 +126,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { return u.URL } -func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.RefreshAttributes) ([]string, error) { +func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { if u.performRefreshArgs == nil { u.performRefreshArgs = make([]*PerformRefreshArgs, 0) } @@ -182,7 +183,7 @@ type TestUpstreamOIDCIdentityProvider struct { PerformRefreshFunc func(ctx context.Context, refreshToken string) (*oauth2.Token, error) - RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error + RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error ValidateTokenAndMergeWithUserInfoFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) @@ -198,7 +199,7 @@ type TestUpstreamOIDCIdentityProvider struct { validateTokenAndMergeWithUserInfoArgs []*ValidateTokenAndMergeWithUserInfoArgs } -var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{} +var _ upstreamprovider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{} func (u *TestUpstreamOIDCIdentityProvider) GetResourceUID() types.UID { return u.ResourceUID @@ -302,7 +303,7 @@ func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, r return u.PerformRefreshFunc(ctx, refreshToken) } -func (u *TestUpstreamOIDCIdentityProvider) RevokeToken(ctx context.Context, token string, tokenType provider.RevocableTokenType) error { +func (u *TestUpstreamOIDCIdentityProvider) RevokeToken(ctx context.Context, token string, tokenType upstreamprovider.RevocableTokenType) error { if u.revokeTokenArgs == nil { u.revokeTokenArgs = make([]*RevokeTokenArgs, 0) } @@ -387,21 +388,21 @@ func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryId func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { idpProvider := provider.NewDynamicUpstreamIDPProvider() - oidcUpstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) + oidcUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) for i := range b.upstreamOIDCIdentityProviders { - oidcUpstreams[i] = provider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) + oidcUpstreams[i] = upstreamprovider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) } idpProvider.SetOIDCIdentityProviders(oidcUpstreams) - ldapUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, len(b.upstreamLDAPIdentityProviders)) + ldapUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamLDAPIdentityProviders)) for i := range b.upstreamLDAPIdentityProviders { - ldapUpstreams[i] = provider.UpstreamLDAPIdentityProviderI(b.upstreamLDAPIdentityProviders[i]) + ldapUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamLDAPIdentityProviders[i]) } idpProvider.SetLDAPIdentityProviders(ldapUpstreams) - adUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, len(b.upstreamActiveDirectoryIdentityProviders)) + adUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamActiveDirectoryIdentityProviders)) for i := range b.upstreamActiveDirectoryIdentityProviders { - adUpstreams[i] = provider.UpstreamLDAPIdentityProviderI(b.upstreamActiveDirectoryIdentityProviders[i]) + adUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamActiveDirectoryIdentityProviders[i]) } idpProvider.SetActiveDirectoryIdentityProviders(adUpstreams) @@ -822,7 +823,7 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent } return u.refreshedTokens, nil }, - RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error { + RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error { return u.revokeTokenErr }, ValidateTokenAndMergeWithUserInfoFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index beb6a037e..7dd0dcc67 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -28,7 +28,7 @@ import ( "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" ) @@ -120,7 +120,7 @@ type ProviderConfig struct { GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error) // RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected. - RefreshAttributeChecks map[string]func(*ldap.Entry, provider.RefreshAttributes) error + RefreshAttributeChecks map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error } // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. @@ -167,7 +167,7 @@ type Provider struct { c ProviderConfig } -var _ provider.UpstreamLDAPIdentityProviderI = &Provider{} +var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &Provider{} var _ authenticators.UserAuthenticator = &Provider{} // New creates a Provider. The config is not a pointer to ensure that a copy of the config is created, @@ -188,7 +188,7 @@ func closeAndLogError(conn Conn, doingWhat string) { } } -func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.RefreshAttributes) ([]string, error) { +func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches userDN := storedRefreshAttributes.DN diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 7de3f8282..02476b253 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -26,7 +26,7 @@ import ( "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/tlsassertions" "go.pinniped.dev/internal/testutil/tlsserver" @@ -661,8 +661,8 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.RefreshAttributes) error{ - "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error { return nil }, } @@ -699,8 +699,8 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.RefreshAttributes) error{ - "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error { return nil }, } @@ -1575,8 +1575,8 @@ func TestUpstreamRefresh(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupSearchGroupNameAttribute, }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ - pwdLastSetAttribute: func(*ldap.Entry, provider.RefreshAttributes) error { return nil }, + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { return nil }, }, } if editFunc != nil { @@ -2280,7 +2280,7 @@ func TestUpstreamRefresh(t *testing.T) { initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) ldapProvider := New(*tt.providerConfig) subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" - groups, err := ldapProvider.PerformRefresh(context.Background(), provider.RefreshAttributes{ + groups, err := ldapProvider.PerformRefresh(context.Background(), upstreamprovider.RefreshAttributes{ Username: testUserSearchResultUsernameAttributeValue, Subject: subject, DN: tt.refreshUserDN, diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index ff8b83fc5..dc61b11f4 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -23,13 +23,14 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" ) -func New(config *oauth2.Config, provider *coreosoidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { +func New(config *oauth2.Config, provider *coreosoidc.Provider, client *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { return &ProviderConfig{Config: config, Provider: provider, Client: client} } @@ -52,7 +53,7 @@ type ProviderConfig struct { } } -var _ provider.UpstreamOIDCIdentityProviderI = (*ProviderConfig)(nil) +var _ upstreamprovider.UpstreamOIDCIdentityProviderI = (*ProviderConfig)(nil) func (p *ProviderConfig) GetResourceUID() types.UID { return p.ResourceUID @@ -160,7 +161,7 @@ func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string // It may return an error wrapped by a RetryableRevocationError, which is an error indicating that it may // be worth trying to revoke the same token again later. Any other error returned should be assumed to // represent an error such that it is not worth retrying revocation later, even though revocation failed. -func (p *ProviderConfig) RevokeToken(ctx context.Context, token string, tokenType provider.RevocableTokenType) error { +func (p *ProviderConfig) RevokeToken(ctx context.Context, token string, tokenType upstreamprovider.RevocableTokenType) error { if p.RevocationURL == nil { plog.Trace("RevokeToken() was called but upstream provider has no available revocation endpoint", "providerName", p.Name, @@ -188,7 +189,7 @@ func (p *ProviderConfig) RevokeToken(ctx context.Context, token string, tokenTyp func (p *ProviderConfig) tryRevokeToken( ctx context.Context, token string, - tokenType provider.RevocableTokenType, + tokenType upstreamprovider.RevocableTokenType, useBasicAuth bool, ) (tryAnotherClientAuthMethod bool, err error) { clientID := p.Config.ClientID diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index 1155deb38..e477f1ba6 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -25,6 +25,7 @@ import ( "go.pinniped.dev/internal/mocks/mockkeyset" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -484,7 +485,7 @@ func TestProviderConfig(t *testing.T) { t.Run("RevokeToken", func(t *testing.T) { tests := []struct { name string - tokenType provider.RevocableTokenType + tokenType upstreamprovider.RevocableTokenType nilRevocationURL bool unreachableServer bool returnStatusCodes []int @@ -496,33 +497,33 @@ func TestProviderConfig(t *testing.T) { }{ { name: "success without calling the server when there is no revocation URL set for refresh token", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, nilRevocationURL: true, wantNumRequests: 0, }, { name: "success without calling the server when there is no revocation URL set for access token", - tokenType: provider.AccessTokenType, + tokenType: upstreamprovider.AccessTokenType, nilRevocationURL: true, wantNumRequests: 0, }, { name: "success when the server returns 200 OK on the first call for refresh token", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusOK}, wantNumRequests: 1, wantTokenTypeHint: "refresh_token", }, { name: "success when the server returns 200 OK on the first call for access token", - tokenType: provider.AccessTokenType, + tokenType: upstreamprovider.AccessTokenType, returnStatusCodes: []int{http.StatusOK}, wantNumRequests: 1, wantTokenTypeHint: "access_token", }, { name: "success when the server returns 400 Bad Request on the first call due to client auth, then 200 OK on second call for refresh token", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest, http.StatusOK}, // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 defines this as the error for client auth failure returnErrBodies: []string{`{ "error":"invalid_client", "error_description":"unhappy" }`}, @@ -531,7 +532,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "success when the server returns 400 Bad Request on the first call due to client auth, then 200 OK on second call for access token", - tokenType: provider.AccessTokenType, + tokenType: upstreamprovider.AccessTokenType, returnStatusCodes: []int{http.StatusBadRequest, http.StatusOK}, // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 defines this as the error for client auth failure returnErrBodies: []string{`{ "error":"invalid_client", "error_description":"unhappy" }`}, @@ -540,7 +541,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when the server returns 400 Bad Request on the first call due to client auth, then any 400 error on second call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest, http.StatusBadRequest}, returnErrBodies: []string{`{ "error":"invalid_client", "error_description":"unhappy" }`, `{ "error":"anything", "error_description":"unhappy" }`}, wantErr: testutil.WantExactErrorString(`server responded with status 400 with body: { "error":"anything", "error_description":"unhappy" }`), @@ -550,7 +551,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when the server returns 400 Bad Request with bad JSON body on the first call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest}, returnErrBodies: []string{`invalid JSON body`}, wantErr: testutil.WantExactErrorString(`error parsing response body "invalid JSON body" on response with status code 400: invalid character 'i' looking for beginning of value`), @@ -560,7 +561,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when the server returns 400 Bad Request with empty body", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest}, returnErrBodies: []string{``}, wantErr: testutil.WantExactErrorString(`error parsing response body "" on response with status code 400: unexpected end of JSON input`), @@ -570,7 +571,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when the server returns 400 Bad Request on the first call due to client auth, then any other error on second call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest, http.StatusForbidden}, returnErrBodies: []string{`{ "error":"invalid_client", "error_description":"unhappy" }`, ""}, wantErr: testutil.WantExactErrorString("server responded with status 403"), @@ -580,7 +581,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when server returns any other 400 error on first call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusBadRequest}, returnErrBodies: []string{`{ "error":"anything_else", "error_description":"unhappy" }`}, wantErr: testutil.WantExactErrorString(`server responded with status 400 with body: { "error":"anything_else", "error_description":"unhappy" }`), @@ -590,7 +591,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "error when server returns any other error aside from 400 on first call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusForbidden}, returnErrBodies: []string{""}, wantErr: testutil.WantExactErrorString("server responded with status 403"), @@ -600,7 +601,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "retryable error when server returns 503 on first call", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusServiceUnavailable}, // 503 returnErrBodies: []string{""}, wantErr: testutil.WantExactErrorString("retryable revocation error: server responded with status 503"), @@ -610,7 +611,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "retryable error when the server returns 400 Bad Request on the first call due to client auth, then 503 on second call", - tokenType: provider.AccessTokenType, + tokenType: upstreamprovider.AccessTokenType, returnStatusCodes: []int{http.StatusBadRequest, http.StatusServiceUnavailable}, // 400, 503 returnErrBodies: []string{`{ "error":"invalid_client", "error_description":"unhappy" }`, ""}, wantErr: testutil.WantExactErrorString("retryable revocation error: server responded with status 503"), @@ -620,7 +621,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "retryable error when server returns any 5xx status on first call, testing lower bound of 5xx range", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{http.StatusInternalServerError}, // 500 returnErrBodies: []string{""}, wantErr: testutil.WantExactErrorString("retryable revocation error: server responded with status 500"), @@ -630,7 +631,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "retryable error when server returns any 5xx status on first call, testing upper bound of 5xx range", - tokenType: provider.RefreshTokenType, + tokenType: upstreamprovider.RefreshTokenType, returnStatusCodes: []int{599}, // not defined by an RFC, but sometimes considered Network Connect Timeout Error returnErrBodies: []string{""}, wantErr: testutil.WantExactErrorString("retryable revocation error: server responded with status 599"), @@ -640,7 +641,7 @@ func TestProviderConfig(t *testing.T) { }, { name: "retryable error when the server cannot be reached", - tokenType: provider.AccessTokenType, + tokenType: upstreamprovider.AccessTokenType, unreachableServer: true, wantErr: testutil.WantMatchingErrorString("^retryable revocation error: Post .*: dial tcp .*: connect: connection refused$"), wantRetryableErrType: true, diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index b75f743e8..341d1ffd9 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -33,7 +33,7 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -107,7 +107,7 @@ type handlerState struct { getEnv func(key string) string listen func(string, string) (net.Listener, error) isTTY func(int) bool - getProvider func(*oauth2.Config, *coreosoidc.Provider, *http.Client) provider.UpstreamOIDCIdentityProviderI + getProvider func(*oauth2.Config, *coreosoidc.Provider, *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI validateIDToken func(ctx context.Context, provider *coreosoidc.Provider, audience string, token string) (*coreosoidc.IDToken, error) promptForValue func(ctx context.Context, promptLabel string) (string, error) promptForSecret func(promptLabel string) (string, error) @@ -196,10 +196,11 @@ func WithSkipListen() Option { // SessionCacheKey contains the data used to select a valid session cache entry. type SessionCacheKey struct { - Issuer string `json:"issuer"` - ClientID string `json:"clientID"` - Scopes []string `json:"scopes"` - RedirectURI string `json:"redirect_uri"` + Issuer string `json:"issuer"` + ClientID string `json:"clientID"` + Scopes []string `json:"scopes"` + RedirectURI string `json:"redirect_uri"` + UpstreamProviderName string `json:"upstream_provider_name,omitempty"` } type SessionCache interface { @@ -351,6 +352,10 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { ClientID: h.clientID, Scopes: h.scopes, RedirectURI: (&url.URL{Scheme: "http", Host: h.listenAddr, Path: h.callbackPath}).String(), + // When using a Supervisor with multiple IDPs, the cache keys need to be different for each IDP + // so a user can have multiple sessions going for each IDP at the same time. + // When using a non-Supervisor OIDC provider, then this value will be blank, so it won't be part of the key. + UpstreamProviderName: h.upstreamIdentityProviderName, } // If the ID token is still valid for a bit, return it immediately and skip the rest of the flow. diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index ab393030f..fa896aa99 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidcclient @@ -32,7 +32,7 @@ import ( "go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" @@ -504,7 +504,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo return func(h *handlerState) error { require.NoError(t, WithClient(newClientForServer(successServer))(h)) - h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false). @@ -553,7 +553,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo return func(h *handlerState) error { require.NoError(t, WithClient(newClientForServer(successServer))(h)) - h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false). @@ -1159,7 +1159,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo fmt.Sprintf("http://127.0.0.1:0/callback?code=%s&state=test-state", fakeAuthCode), }}, }, nil) - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens( @@ -1181,7 +1181,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo return func(h *handlerState) error { fakeAuthCode := "test-authcode-value" - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens( @@ -1281,7 +1281,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo return func(h *handlerState) error { fakeAuthCode := "test-authcode-value" - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens( @@ -1392,7 +1392,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo return func(h *handlerState) error { fakeAuthCode := "test-authcode-value" - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens( @@ -1855,7 +1855,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo }) h.cache = cache - h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false). @@ -1993,7 +1993,7 @@ func TestHandlePasteCallback(t *testing.T) { return "invalid", nil } h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "invalid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). @@ -2017,7 +2017,7 @@ func TestHandlePasteCallback(t *testing.T) { return "valid", nil } h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). @@ -2237,7 +2237,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { opt: func(t *testing.T) Option { return func(h *handlerState) error { h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "invalid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). @@ -2256,7 +2256,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { opt: func(t *testing.T) Option { return func(h *handlerState) error { h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). @@ -2282,7 +2282,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { return func(h *handlerState) error { h.useFormPost = true h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). @@ -2311,7 +2311,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { return func(h *handlerState) error { h.useFormPost = true h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI} - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) upstreamprovider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI). From 32aa015d5bbd7aeb93d3223a45c396a9eabf76cb Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 5 Jun 2023 14:40:39 -0700 Subject: [PATCH 05/81] Fixup unit tests for the previous commit --- .../federation_domain_watcher_test.go | 69 +++++++++++-------- .../garbage_collector_test.go | 30 ++++---- .../accesstoken/accesstoken_test.go | 30 ++++---- .../authorizationcode/authorizationcode.go | 44 +++++++----- .../authorizationcode_test.go | 34 ++++----- .../openidconnect/openidconnect_test.go | 8 +-- internal/fositestorage/pkce/pkce_test.go | 8 +-- .../refreshtoken/refreshtoken_test.go | 32 +++++---- .../provider/federation_domain_issuer_test.go | 10 ++- internal/testutil/psession.go | 12 ++-- pkg/oidcclient/login_test.go | 47 +++++++------ 11 files changed, 180 insertions(+), 144 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index bf19af469..980ecac69 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorconfig @@ -42,11 +42,17 @@ func TestInformerFilters(t *testing.T) { r = require.New(t) observableWithInformerOption = testutil.NewObservableWithInformerOption() federationDomainInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).Config().V1alpha1().FederationDomains() + oidcIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders() + ldapIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders() + adIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders() _ = NewFederationDomainWatcherController( nil, nil, nil, federationDomainInformer, + oidcIdentityProviderInformer, + ldapIdentityProviderInformer, + adIdentityProviderInformer, observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters ) configMapInformerFilter = observableWithInformerOption.GetFilterForInformer(federationDomainInformer) @@ -100,8 +106,8 @@ func TestSync(t *testing.T) { var r *require.Assertions var subject controllerlib.Controller - var federationDomainInformerClient *pinnipedfake.Clientset - var federationDomainInformers pinnipedinformers.SharedInformerFactory + var pinnipedInformerClient *pinnipedfake.Clientset + var pinnipedInformers pinnipedinformers.SharedInformerFactory var pinnipedAPIClient *pinnipedfake.Clientset var cancelContext context.Context var cancelContextCancelFunc context.CancelFunc @@ -118,7 +124,10 @@ func TestSync(t *testing.T) { providersSetter, clocktesting.NewFakeClock(frozenNow), pinnipedAPIClient, - federationDomainInformers.Config().V1alpha1().FederationDomains(), + pinnipedInformers.Config().V1alpha1().FederationDomains(), + pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), controllerlib.WithInformer, ) @@ -133,7 +142,7 @@ func TestSync(t *testing.T) { } // Must start informers before calling TestRunSynchronously() - federationDomainInformers.Start(cancelContext.Done()) + pinnipedInformers.Start(cancelContext.Done()) controllerlib.TestRunSynchronously(t, subject) } @@ -145,8 +154,8 @@ func TestSync(t *testing.T) { cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background()) - federationDomainInformerClient = pinnipedfake.NewSimpleClientset() - federationDomainInformers = pinnipedinformers.NewSharedInformerFactory(federationDomainInformerClient, 0) + pinnipedInformerClient = pinnipedfake.NewSimpleClientset() + pinnipedInformers = pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0) pinnipedAPIClient = pinnipedfake.NewSimpleClientset() federationDomainGVR = schema.GroupVersionResource{ @@ -172,14 +181,14 @@ func TestSync(t *testing.T) { Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain1)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomain1)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain1)) federationDomain2 = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain2)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomain2)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain2)) }) it("calls the ProvidersSetter", func() { @@ -187,10 +196,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer) + provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer) + provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -250,7 +259,7 @@ func TestSync(t *testing.T) { federationDomain1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) r.NoError(pinnipedAPIClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) - r.NoError(federationDomainInformerClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) + r.NoError(pinnipedInformerClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) }) it("only updates the out-of-date FederationDomain", func() { @@ -288,10 +297,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer) + provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer) + provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -326,10 +335,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer) + provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer) + provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -393,7 +402,7 @@ func TestSync(t *testing.T) { Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomain)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) }) when("there is a conflict while updating an FederationDomain", func() { @@ -530,14 +539,14 @@ func TestSync(t *testing.T) { Spec: v1alpha1.FederationDomainSpec{Issuer: "https://valid-issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(validFederationDomain)) - r.NoError(federationDomainInformerClient.Tracker().Add(validFederationDomain)) + r.NoError(pinnipedInformerClient.Tracker().Add(validFederationDomain)) invalidFederationDomain = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } r.NoError(pinnipedAPIClient.Tracker().Add(invalidFederationDomain)) - r.NoError(federationDomainInformerClient.Tracker().Add(invalidFederationDomain)) + r.NoError(pinnipedInformerClient.Tracker().Add(invalidFederationDomain)) }) it("calls the ProvidersSetter with the valid provider", func() { @@ -545,7 +554,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer) + validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -619,7 +628,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer) + validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -688,20 +697,20 @@ func TestSync(t *testing.T) { Spec: v1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate1)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainDuplicate1)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate1)) federationDomainDuplicate2 = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate2)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainDuplicate2)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate2)) federationDomain = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomain)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) }) it("calls the ProvidersSetter with the non-duplicate", func() { @@ -709,7 +718,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer) + nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) @@ -838,7 +847,7 @@ func TestSync(t *testing.T) { }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress1)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) federationDomainSameIssuerAddress2 = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "provider2", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{ @@ -849,7 +858,7 @@ func TestSync(t *testing.T) { }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress2)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) federationDomainDifferentIssuerAddress = &v1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressProvider", Namespace: namespace}, @@ -859,7 +868,7 @@ func TestSync(t *testing.T) { }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDifferentIssuerAddress)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainDifferentIssuerAddress)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDifferentIssuerAddress)) // Also add one with a URL that cannot be parsed to make sure that the error handling // for the duplicate issuers and secret names are not confused by invalid URLs. @@ -874,7 +883,7 @@ func TestSync(t *testing.T) { }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) - r.NoError(federationDomainInformerClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) + r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) }) it("calls the ProvidersSetter with the non-duplicate", func() { @@ -882,7 +891,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer) + nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) r.True(providersSetter.SetProvidersWasCalled) diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index 2266725bc..4eabbad58 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -264,7 +264,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired authcode secrets which contain upstream refresh tokens", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -309,7 +309,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret)) inactiveOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: false, Request: &fosite.Request{ ID: "request-id-2", @@ -388,7 +388,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired authcode secrets which contain upstream access tokens", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -433,7 +433,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret)) inactiveOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: false, Request: &fosite.Request{ ID: "request-id-2", @@ -512,7 +512,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is an invalid, expired authcode secret", func() { it.Before(func() { invalidOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "", // it is invalid for there to be a missing request ID @@ -581,7 +581,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, expired authcode secret but its upstream name does not match any existing upstream", func() { it.Before(func() { wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -652,7 +652,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, expired authcode secret but its upstream UID does not match any existing upstream", func() { it.Before(func() { wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -723,7 +723,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, recently expired authcode secret but the upstream revocation fails", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -828,7 +828,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, long-since expired authcode secret but the upstream revocation fails", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -907,7 +907,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired access token secrets which contain upstream refresh tokens", func() { it.Before(func() { offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"}, ID: "request-id-1", @@ -952,7 +952,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret)) offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2"}, ID: "request-id-2", @@ -1031,7 +1031,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired access token secrets which contain upstream access tokens", func() { it.Before(func() { offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"}, ID: "request-id-1", @@ -1076,7 +1076,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret)) offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2"}, ID: "request-id-2", @@ -1155,7 +1155,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired refresh secrets which contain upstream refresh tokens", func() { it.Before(func() { oidcRefreshSession := &refreshtoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ ID: "request-id-1", Client: &clientregistry.Client{}, @@ -1232,7 +1232,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired refresh secrets which contain upstream access tokens", func() { it.Before(func() { oidcRefreshSession := &refreshtoken.Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ ID: "request-id-1", Client: &clientregistry.Client{}, diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index 9f8e9da0d..52a731f30 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package accesstoken @@ -54,7 +54,7 @@ func TestAccessTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -123,7 +123,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -196,7 +196,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 4") + require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 5") } func TestNilSessionRequest(t *testing.T) { @@ -214,7 +214,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"4"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -298,13 +298,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", }, wantSession: &Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ ID: "abcd-1", Client: &clientregistry.Client{}, @@ -316,10 +316,12 @@ func TestReadFromSecret(t *testing.T) { Headers: &jwt.Headers{Extra: map[string]interface{}{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ - Username: "fake-username", - ProviderUID: "fake-provider-uid", - ProviderName: "fake-provider-name", - ProviderType: "fake-provider-type", + Username: "fake-username", + ProviderUID: "fake-provider-uid", + ProviderName: "fake-provider-name", + ProviderType: "fake-provider-type", + UpstreamUsername: "fake-upstream-username", + UpstreamGroups: []string{"fake-upstream-group1", "fake-upstream-group2"}, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: "fake-upstream-refresh-token", }, @@ -339,7 +341,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"3","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-access-token", @@ -362,7 +364,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/access-token", }, - wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of 4", + wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of 5", }, { name: "missing request", @@ -375,7 +377,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index abfa9e398..6c451acfd 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -373,42 +373,48 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ }, "custom": { "username": "Ĝ眧Ĭ", - "providerUID": "ʼn2ƋŢ觛ǂ焺nŐǛ", - "providerName": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4", - "providerType": "ȣ掘ʃƸ澺淗a紽ǒ|鰽", + "upstreamUsername": "ʼn2ƋŢ觛ǂ焺nŐǛ", + "upstreamGroups": [ + "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt", + "ʃƸ澺淗a紽ǒ|鰽ŋ猊Ia瓕巈環_ɑ" + ], + "providerUID": "ƴŤȱʀļÂ?墖", + "providerName": "7就伒犘c钡", + "providerType": "k|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮", "warnings": [ - "t毇妬\u003e6鉢緋uƴŤȱʀļÂ", - "虝27就伒犘c钡ɏȫ齁š" + "鷞aŚB碠k9帴ʘ赱", + "ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔" ], "oidc": { - "upstreamRefreshToken": "OpKȱ藚ɏ¬Ê蒭堜]ȗ韚ʫ繕ȫ碰+ʫ", - "upstreamAccessToken": "k9帴", - "upstreamSubject": "磊ůď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ", - "upstreamIssuer": "4İ" + "upstreamRefreshToken": "墀jMʥ", + "upstreamAccessToken": "+î艔垎0", + "upstreamSubject": "ĝ", + "upstreamIssuer": "ǢIȽ" }, "ldap": { - "userDN": "×", + "userDN": "士b", "extraRefreshAttributes": { - "ʥ笿0D": "s" + "O灞浛a齙\\蹼偦歛ơ 皦pSǬŝ": "Džķ?吭匞饫Ƽĝ\"zvư", + "f跞@)¿,ɭS隑ip偶宾儮猷": "面@yȝƋ鬯犦獢9c5¤" } }, "activedirectory": { - "userDN": "ĝ", + "userDN": "置b", "extraRefreshAttributes": { - "IȽ齤士bEǎ": "跞@)¿,ɭS隑ip偶宾儮猷V麹", - "ȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛" + "MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{鼐": "$+溪ŸȢŒų崓ļ憽", + "ĩŦʀ宍D挟": "q萮左/篣AÚƄŕ~čfVLPC諡}", + "姧骦:駝重EȫʆɵʮGɃ": "囤1+,Ȳ齠@ɍB鳛Nč乿ƔǴę鏶" } } } }, "requestedAudience": [ - " 皦pSǬŝ社Vƅȭǝ*擦28Dž", - "vư" + "ň" ], "grantedAudience": [ - "置b", - "筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{" + "â融貵捠ʼn", + "d鞕ȸ腿tʏƲ%}ſ¯Ɣ 籌Tǘ乚Ȥ2" ] }, - "version": "4" + "version": "5" }` diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index 05f206718..355800018 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package authorizationcode @@ -66,7 +66,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -86,7 +86,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -204,7 +204,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 4") + require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 5") } func TestNilSessionRequest(t *testing.T) { @@ -219,7 +219,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"4", "active": true}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"5", "active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -386,7 +386,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // set these to match CreateAuthorizeCodeSession so that .JSONEq works validSession.Active = true - validSession.Version = "4" + validSession.Version = "5" // update this when you update the storage version in the production code validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t") require.NoError(t, err) @@ -395,7 +395,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // the fuzzed session and storage session should have identical JSON require.JSONEq(t, authorizeCodeSessionJSONFromFuzzing, authorizeCodeSessionJSONFromStorage) - // t.Log("actual value from fuzzing", authorizeCodeSessionJSONFromFuzzing) // can be useful when updating expected value + t.Log("actual value from fuzzing", authorizeCodeSessionJSONFromFuzzing) // can be useful when updating expected value // while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed, // if it adds a new field that can be fuzzed, this check will fail @@ -421,13 +421,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", }, wantSession: &Session{ - Version: "4", + Version: "5", Active: true, Request: &fosite.Request{ ID: "abcd-1", @@ -440,10 +440,12 @@ func TestReadFromSecret(t *testing.T) { Headers: &jwt.Headers{Extra: map[string]interface{}{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ - Username: "fake-username", - ProviderUID: "fake-provider-uid", - ProviderName: "fake-provider-name", - ProviderType: "fake-provider-type", + Username: "fake-username", + ProviderUID: "fake-provider-uid", + ProviderName: "fake-provider-name", + ProviderType: "fake-provider-type", + UpstreamUsername: "fake-upstream-username", + UpstreamGroups: []string{"fake-upstream-group1", "fake-upstream-group2"}, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: "fake-upstream-refresh-token", }, @@ -463,7 +465,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"3","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-authcode", @@ -486,7 +488,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/authcode", }, - wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 4", + wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 5", }, { name: "missing request", @@ -499,7 +501,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 7297710b7..e4740ac73 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package openidconnect @@ -52,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -137,7 +137,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil) - require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 4") + require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 5") } func TestNilSessionRequest(t *testing.T) { @@ -152,7 +152,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"4"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index 46461611f..f0a24fd4a 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package pkce @@ -52,7 +52,7 @@ func TestPKCEStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -140,7 +140,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetPKCERequestSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 4") + require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 5") } func TestNilSessionRequest(t *testing.T) { @@ -158,7 +158,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"4"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index e785347da..8e2826b9e 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package refreshtoken @@ -53,7 +53,7 @@ func TestRefreshTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -123,7 +123,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -178,7 +178,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"4"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -252,7 +252,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 4") + require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 5") } func TestNilSessionRequest(t *testing.T) { @@ -270,7 +270,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"4"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"5"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -354,13 +354,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", }, wantSession: &Session{ - Version: "4", + Version: "5", Request: &fosite.Request{ ID: "abcd-1", Client: &clientregistry.Client{}, @@ -372,10 +372,12 @@ func TestReadFromSecret(t *testing.T) { Headers: &jwt.Headers{Extra: map[string]interface{}{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ - Username: "fake-username", - ProviderUID: "fake-provider-uid", - ProviderName: "fake-provider-name", - ProviderType: "fake-provider-type", + Username: "fake-username", + ProviderUID: "fake-provider-uid", + ProviderName: "fake-provider-name", + ProviderType: "fake-provider-type", + UpstreamUsername: "fake-upstream-username", + UpstreamGroups: []string{"fake-upstream-group1", "fake-upstream-group2"}, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: "fake-upstream-refresh-token", }, @@ -395,7 +397,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-refresh-token", @@ -418,7 +420,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/refresh-token", }, - wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of 4", + wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of 5", }, { name: "missing request", @@ -431,7 +433,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"4","active": true}`), + "pinniped-storage-data": []byte(`{"version":"5","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", diff --git a/internal/oidc/provider/federation_domain_issuer_test.go b/internal/oidc/provider/federation_domain_issuer_test.go index 7f10dd331..b80bff196 100644 --- a/internal/oidc/provider/federation_domain_issuer_test.go +++ b/internal/oidc/provider/federation_domain_issuer_test.go @@ -82,7 +82,15 @@ func TestFederationDomainIssuerValidations(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - _, err := NewFederationDomainIssuer(tt.issuer) + _, err := NewFederationDomainIssuer(tt.issuer, nil) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + } else { + require.NoError(t, err) + } + + // This alternate constructor should perform all the same validations on the issuer string. + _, err = NewFederationDomainIssuerWithDefaultIDP(tt.issuer, nil) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) } else { diff --git a/internal/testutil/psession.go b/internal/testutil/psession.go index 88eb658b7..8efbcf250 100644 --- a/internal/testutil/psession.go +++ b/internal/testutil/psession.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil @@ -24,10 +24,12 @@ func NewFakePinnipedSession() *psession.PinnipedSession { Subject: "panda", }, Custom: &psession.CustomSessionData{ - Username: "fake-username", - ProviderUID: "fake-provider-uid", - ProviderType: "fake-provider-type", - ProviderName: "fake-provider-name", + Username: "fake-username", + ProviderUID: "fake-provider-uid", + ProviderType: "fake-provider-type", + ProviderName: "fake-provider-name", + UpstreamUsername: "fake-upstream-username", + UpstreamGroups: []string{"fake-upstream-group1", "fake-upstream-group2"}, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: "fake-upstream-refresh-token", UpstreamSubject: "some-subject", diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index fa896aa99..e0b08adc1 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -321,10 +321,11 @@ func TestLogin(t *testing.T) { //nolint:gocyclo cache := &mockSessionCache{t: t, getReturnsToken: nil} cacheKey := SessionCacheKey{ - Issuer: successServer.URL, - ClientID: "test-client-id", - Scopes: []string{"test-scope"}, - RedirectURI: "http://localhost:0/callback", + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: "some-upstream-name", } t.Cleanup(func() { require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) @@ -916,7 +917,7 @@ func TestLogin(t *testing.T) { //nolint:gocyclo wantToken: &testToken, }, { - name: "upstream name and type are included in authorize request if upstream name is provided", + name: "upstream name and type are included in authorize request and session cache key if upstream name is provided", clientID: "test-client-id", opt: func(t *testing.T) Option { return func(h *handlerState) error { @@ -926,10 +927,11 @@ func TestLogin(t *testing.T) { //nolint:gocyclo cache := &mockSessionCache{t: t, getReturnsToken: nil} cacheKey := SessionCacheKey{ - Issuer: successServer.URL, - ClientID: "test-client-id", - Scopes: []string{"test-scope"}, - RedirectURI: "http://localhost:0/callback", + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: "some-upstream-name", } t.Cleanup(func() { require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) @@ -1207,10 +1209,11 @@ func TestLogin(t *testing.T) { //nolint:gocyclo cache := &mockSessionCache{t: t, getReturnsToken: nil} cacheKey := SessionCacheKey{ - Issuer: successServer.URL, - ClientID: "test-client-id", - Scopes: []string{"test-scope"}, - RedirectURI: "http://localhost:0/callback", + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: "some-upstream-name", } t.Cleanup(func() { require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) @@ -1314,10 +1317,11 @@ func TestLogin(t *testing.T) { //nolint:gocyclo cache := &mockSessionCache{t: t, getReturnsToken: nil} cacheKey := SessionCacheKey{ - Issuer: successServer.URL, - ClientID: "test-client-id", - Scopes: []string{"test-scope"}, - RedirectURI: "http://localhost:0/callback", + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: "some-upstream-name", } t.Cleanup(func() { require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) @@ -1425,10 +1429,11 @@ func TestLogin(t *testing.T) { //nolint:gocyclo cache := &mockSessionCache{t: t, getReturnsToken: nil} cacheKey := SessionCacheKey{ - Issuer: successServer.URL, - ClientID: "test-client-id", - Scopes: []string{"test-scope"}, - RedirectURI: "http://localhost:0/callback", + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: "some-upstream-name", } t.Cleanup(func() { require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) From b7627208ea9453793b19f4738ecb256206a01446 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Wed, 7 Jun 2023 17:22:02 -0400 Subject: [PATCH 06/81] Add tests for identity_transformation.go Co-authored-by: Ryan Richard --- internal/celtransformer/celformer_test.go | 30 ++ .../idtransform/identity_transformations.go | 8 +- .../identity_transformations_test.go | 262 ++++++++++++++++++ 3 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 internal/idtransform/identity_transformations_test.go diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 54724a416..2bcdc96df 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -298,6 +298,36 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"admins", "developers", "other", "new-group"}, }, + { + name: "a nil passed as groups will be converted to an empty list", + username: "ryan", + groups: nil, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups`}, + }, + wantUsername: "ryan", + wantGroups: []string{}, + }, + { + name: "a nil passed as groups will be converted to an empty list and can be used with CEL operators", + username: "ryan", + groups: nil, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups == [] ? ["the-groups-list-was-an-empty-list"] : ["the-groups-list-was-not-an-empty-list"]`}, + }, + wantUsername: "ryan", + wantGroups: []string{"the-groups-list-was-an-empty-list"}, + }, + { + name: "an empty list of groups is allowed", + username: "ryan", + groups: []string{}, + transforms: []CELTransformation{ + &GroupsTransformation{Expression: `groups`}, + }, + wantUsername: "ryan", + wantGroups: []string{}, + }, { name: "can add a group from a const", username: "ryan", diff --git a/internal/idtransform/identity_transformations.go b/internal/idtransform/identity_transformations.go index d7fb4d31f..095b98f81 100644 --- a/internal/idtransform/identity_transformations.go +++ b/internal/idtransform/identity_transformations.go @@ -47,13 +47,16 @@ func (p *TransformationPipeline) AppendTransformation(t IdentityTransformation) // short-circuited but no error is returned. Only unexpected errors are returned as errors. This is safe to call // from multiple goroutines. func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + if groups == nil { + groups = []string{} + } accumulatedResult := &TransformationResult{ Username: username, Groups: groups, AuthenticationAllowed: true, } - var err error for i, transform := range p.transforms { + var err error accumulatedResult, err = transform.Evaluate(ctx, accumulatedResult.Username, accumulatedResult.Groups) if err != nil { // There was an unexpected error evaluating a transformation. @@ -66,6 +69,9 @@ func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, if strings.TrimSpace(accumulatedResult.Username) == "" { return nil, fmt.Errorf("identity transformation returned an empty username, which is not allowed") } + if accumulatedResult.Groups == nil { + return nil, fmt.Errorf("identity transformation returned a null list of groups, which is not allowed") + } } // There were no unexpected errors and no policy which rejected auth. return accumulatedResult, nil diff --git a/internal/idtransform/identity_transformations_test.go b/internal/idtransform/identity_transformations_test.go new file mode 100644 index 000000000..a19b6febb --- /dev/null +++ b/internal/idtransform/identity_transformations_test.go @@ -0,0 +1,262 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package idtransform + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +type FakeNoopTransformer struct{} + +func (a FakeNoopTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + return &TransformationResult{ + Username: username, + Groups: groups, + AuthenticationAllowed: true, + RejectedAuthenticationMessage: "none", + }, nil +} + +type FakeNilGroupTransformer struct{} + +func (a FakeNilGroupTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + return &TransformationResult{ + Username: username, + Groups: nil, + AuthenticationAllowed: true, + RejectedAuthenticationMessage: "none", + }, nil +} + +type FakeAppendStringTransformer struct{} + +func (a FakeAppendStringTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + newGroups := []string{} + for _, group := range groups { + newGroups = append(newGroups, group+":transformed") + } + return &TransformationResult{ + Username: username + ":transformed", + Groups: newGroups, + AuthenticationAllowed: true, + RejectedAuthenticationMessage: "none", + }, nil +} + +type FakeDeleteUsernameAndGroupsTransformer struct{} + +func (d FakeDeleteUsernameAndGroupsTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + return &TransformationResult{ + Username: "", + Groups: []string{}, + AuthenticationAllowed: true, + RejectedAuthenticationMessage: "none", + }, nil +} + +type FakeAuthenticationDisallowedTransformer struct{} + +func (d FakeAuthenticationDisallowedTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + newGroups := []string{} + for _, group := range groups { + newGroups = append(newGroups, group+":disallowed") + } + return &TransformationResult{ + Username: username + ":disallowed", + Groups: newGroups, + AuthenticationAllowed: false, + RejectedAuthenticationMessage: "no authentication is allowed", + }, nil +} + +type FakeErrorTransformer struct{} + +func (d FakeErrorTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + return &TransformationResult{}, errors.New("unexpected catastrophic error") +} + +func TestTransformationPipeline(t *testing.T) { + tests := []struct { + name string + username string + groups []string + transforms []IdentityTransformation + wantUsername string + wantGroups []string + wantAuthenticationAllowed bool + wantRejectionAuthenticationMessage string + wantError string + }{ + { + name: "single transformation applied successfully", + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + }, + username: "foo", + groups: []string{ + "foobar", + "foobaz", + }, + wantUsername: "foo:transformed", + wantGroups: []string{ + "foobar:transformed", + "foobaz:transformed", + }, + wantAuthenticationAllowed: true, + wantRejectionAuthenticationMessage: "none", + }, + { + name: "multiple transformations applied successfully", + username: "foo", + groups: []string{ + "foobar", + "foobaz", + }, + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + FakeAppendStringTransformer{}, + }, + wantUsername: "foo:transformed:transformed", + wantGroups: []string{ + "foobar:transformed:transformed", + "foobaz:transformed:transformed", + }, + wantAuthenticationAllowed: true, + wantRejectionAuthenticationMessage: "none", + }, + { + name: "single transformation results in AuthenticationAllowed:false", + username: "foo", + groups: []string{ + "foobar", + }, + transforms: []IdentityTransformation{ + FakeAuthenticationDisallowedTransformer{}, + }, + wantUsername: "foo:disallowed", + wantGroups: []string{"foobar:disallowed"}, + wantAuthenticationAllowed: false, + wantRejectionAuthenticationMessage: "no authentication is allowed", + }, + { + name: "multiple transformations results in AuthenticationAllowed:false but earlier transforms are successful", + username: "foo", + groups: []string{ + "foobar", + }, + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + FakeAuthenticationDisallowedTransformer{}, + // this transformation will not be run because the previous exits the pipeline + FakeAppendStringTransformer{}, + }, + wantUsername: "foo:transformed:disallowed", + wantGroups: []string{"foobar:transformed:disallowed"}, + wantAuthenticationAllowed: false, + wantRejectionAuthenticationMessage: "no authentication is allowed", + }, + { + name: "unexpected error at index", + username: "foo", + groups: []string{"foobar"}, + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + FakeErrorTransformer{}, + FakeAppendStringTransformer{}, + }, + wantError: "identity transformation at index 1: unexpected catastrophic error", + }, + { + name: "empty username not allowed", + username: "foo", + transforms: []IdentityTransformation{ + FakeDeleteUsernameAndGroupsTransformer{}, + }, + wantError: "identity transformation returned an empty username, which is not allowed", + }, + { + name: "whitespace username not allowed", + username: " \t\n\r ", + transforms: []IdentityTransformation{ + FakeNoopTransformer{}, + }, + wantError: "identity transformation returned an empty username, which is not allowed", + }, + { + name: "identity transformation which returns an empty list of groups is allowed", + username: "foo", + groups: []string{}, + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + }, + wantUsername: "foo:transformed", + wantGroups: []string{}, + wantAuthenticationAllowed: true, + wantRejectionAuthenticationMessage: "none", + }, + { + name: "nil passed in for groups will be automatically converted to an empty list", + username: "foo", + groups: nil, + transforms: []IdentityTransformation{ + FakeNoopTransformer{}, + }, + wantUsername: "foo", + wantGroups: []string{}, + wantAuthenticationAllowed: true, + wantRejectionAuthenticationMessage: "none", + }, + { + name: "any transformation returning nil for the list of groups will cause an error", + username: "foo", + groups: []string{"these.will.be.converted.to.nil"}, + transforms: []IdentityTransformation{ + FakeNilGroupTransformer{}, + }, + wantError: "identity transformation returned a null list of groups, which is not allowed", + }, + { + name: "no transformations is allowed", + username: "foo", + groups: []string{"bar", "baz"}, + transforms: []IdentityTransformation{}, + wantUsername: "foo", + wantGroups: []string{"bar", "baz"}, + wantAuthenticationAllowed: true, + // since no transformations run, this will be empty string + wantRejectionAuthenticationMessage: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + pipeline := NewTransformationPipeline() + + for _, transform := range tt.transforms { + pipeline.AppendTransformation(transform) + } + + result, err := pipeline.Evaluate(context.Background(), tt.username, tt.groups) + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + require.Nil(t, result) + return + } + + require.NoError(t, err, "got an unexpected evaluation error") + require.Equal(t, tt.wantUsername, result.Username) + require.Equal(t, tt.wantGroups, result.Groups) + require.Equal(t, tt.wantAuthenticationAllowed, result.AuthenticationAllowed) + require.Equal(t, tt.wantRejectionAuthenticationMessage, result.RejectedAuthenticationMessage) + }) + } +} From 96098841dd3314644252aabd10ac45a66f8d97a3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Jun 2023 12:26:59 -0700 Subject: [PATCH 07/81] Get tests to compile again and fix lint errors --- cmd/pinniped/cmd/login_oidc.go | 2 +- internal/celtransformer/celformer.go | 2 +- .../impersonator_config_test.go | 2 +- .../controller/kubecertagent/kubecertagent.go | 2 +- .../federation_domain_watcher.go | 4 +- .../oidc_upstream_watcher_test.go | 2 +- .../garbage_collector_test.go | 24 +- .../controllermanager/prepare_controllers.go | 12 +- internal/oidc/auth/auth_handler_test.go | 160 +++++----- .../oidc/callback/callback_handler_test.go | 14 +- .../idp_discovery_handler_test.go | 38 ++- .../oidc/login/post_login_handler_test.go | 124 ++++---- ...ration_domain_identity_providers_lister.go | 5 + .../provider/federation_domain_issuer_test.go | 10 +- .../oidc/provider/manager/manager_test.go | 46 ++- internal/oidc/token/token_handler.go | 3 +- internal/oidc/token/token_handler_test.go | 263 ++++++++-------- internal/supervisor/server/server.go | 2 +- .../testutil/oidctestutil/oidctestutil.go | 288 ++++++++++++++++-- pkg/oidcclient/login_test.go | 2 +- 20 files changed, 624 insertions(+), 381 deletions(-) diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 69b8951cc..ac76cb4f8 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -164,7 +164,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin // Initialize the login handler. opts := []oidcclient.Option{ oidcclient.WithContext(cmd.Context()), - oidcclient.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements + oidcclient.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements oidcclient.WithScopes(flags.scopes), oidcclient.WithSessionCache(sessionCache), } diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go index 133555a11..2c1bf409d 100644 --- a/internal/celtransformer/celformer.go +++ b/internal/celtransformer/celformer.go @@ -59,7 +59,7 @@ type TransformationConstants struct { StringListConstants map[string][]string } -// Valid identifiers in CEL expressions are defined by the CEL language spec as: [_a-zA-Z][_a-zA-Z0-9]* +// Valid identifiers in CEL expressions are defined as [_a-zA-Z][_a-zA-Z0-9]* by the CEL language spec. var validIdentifiersRegexp = regexp.MustCompile(`^[_a-zA-Z][_a-zA-Z0-9]*$`) func (t *TransformationConstants) validateVariableNames() error { diff --git a/internal/controller/impersonatorconfig/impersonator_config_test.go b/internal/controller/impersonatorconfig/impersonator_config_test.go index 25252afcd..8ec62f59d 100644 --- a/internal/controller/impersonatorconfig/impersonator_config_test.go +++ b/internal/controller/impersonatorconfig/impersonator_config_test.go @@ -92,7 +92,7 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { nil, caSignerName, nil, - plog.Logr(), //nolint:staticcheck // old test with no log assertions + plog.Logr(), //nolint:staticcheck // old test with no log assertions ) credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer) servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer) diff --git a/internal/controller/kubecertagent/kubecertagent.go b/internal/controller/kubecertagent/kubecertagent.go index 30faa3b5b..d3e414940 100644 --- a/internal/controller/kubecertagent/kubecertagent.go +++ b/internal/controller/kubecertagent/kubecertagent.go @@ -179,7 +179,7 @@ func NewAgentController( dynamicCertProvider, &clock.RealClock{}, cache.NewExpiring(), - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ) } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 9df425086..813667468 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -103,7 +103,7 @@ func NewFederationDomainWatcherController( } // Sync implements controllerlib.Syncer. -func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { +func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { //nolint:funlen,gocyclo federationDomains, err := c.federationDomainInformer.Lister().List(labels.Everything()) if err != nil { return err @@ -325,7 +325,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) // TODO: handle err resultWasAuthRejected := !result.AuthenticationAllowed - if e.Expects.Rejected && !resultWasAuthRejected { + if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif // TODO: handle this failed example plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", "federationDomain", federationDomain.Name, diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 814b425ae..c8e392aef 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -93,7 +93,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { nil, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, - plog.Logr(), //nolint:staticcheck // old test with no log assertions + plog.Logr(), //nolint:staticcheck // old test with no log assertions withInformer.WithInformer, ) diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index 4eabbad58..1e23b1436 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -361,7 +361,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is only revoked for the active authcode session. @@ -485,7 +485,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is only revoked for the active authcode session. @@ -562,7 +562,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Nothing to revoke since we couldn't read the invalid secret. @@ -633,7 +633,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Nothing to revoke since we couldn't find the upstream in the cache. @@ -704,7 +704,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Nothing to revoke since we couldn't find the upstream in the cache. @@ -777,7 +777,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(provider.NewRetryableRevocationError(errors.New("some retryable upstream revocation error"))) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Tried to revoke it, although this revocation will fail. @@ -802,7 +802,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(errors.New("some upstream revocation error not worth retrying")) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Tried to revoke it, although this revocation will fail. @@ -881,7 +881,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(errors.New("some upstream revocation error")) // the upstream revocation will fail idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // Tried to revoke it, although this revocation will fail. @@ -1004,7 +1004,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is only revoked for the downstream session which had offline_access granted. @@ -1128,7 +1128,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is only revoked for the downstream session which had offline_access granted. @@ -1206,7 +1206,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is revoked. @@ -1283,7 +1283,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithRevokeTokenError(nil) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) - startInformersAndController(idpListerBuilder.Build()) + startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) // The upstream refresh token is revoked. diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index 041ebe32d..92dc1e1e1 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package controllermanager provides an entrypoint into running all of the controllers that run as @@ -222,7 +222,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol agentConfig, client, informers.installationNamespaceK8s.Core().V1().Pods(), - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -232,7 +232,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol webhookcachefiller.New( c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -240,7 +240,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol jwtcachefiller.New( c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -249,7 +249,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -275,7 +275,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol impersonator.New, c.NamesConfig.ImpersonationSignerSecret, c.ImpersonationSigningCertProvider, - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements ), singletonWorker, ). diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 308adcc42..f7ff3a88b 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -35,7 +35,6 @@ import ( "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/oidcclientvalidator" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -323,27 +322,26 @@ func TestAuthorizationEndpoint(t *testing.T) { return nil, false, nil } - upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: parsedUpstreamLDAPURL, - AuthenticateFunc: ldapAuthenticateFunc, - } - - upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: parsedUpstreamLDAPURL, - AuthenticateFunc: ldapAuthenticateFunc, - } - - erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { + upstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc). + Build() + + upstreamActiveDirectoryIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc). + Build() + + erroringUpstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithAuthenticateFunc(func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { return nil, false, fmt.Errorf("some ldap upstream auth error") - }, - } + }).Build() happyCSRF := "test-csrf" happyPKCE := "test-pkce" @@ -622,7 +620,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -639,7 +637,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -657,7 +655,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -674,7 +672,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -776,7 +774,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -797,7 +795,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "ActiveDirectory cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -835,7 +833,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -852,7 +850,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -908,7 +906,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -927,7 +925,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -947,7 +945,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -966,7 +964,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1010,7 +1008,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1033,7 +1031,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1213,7 +1211,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client @@ -1332,7 +1330,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error during upstream LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1343,7 +1341,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error during upstream Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1377,7 +1375,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream password for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1389,7 +1387,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream password for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1401,7 +1399,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream username for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To("wrong-username"), @@ -1413,7 +1411,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream username for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To("wrong-username"), @@ -1437,7 +1435,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream username but has password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: nil, // do not send header @@ -1449,7 +1447,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream username on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: nil, // do not send header @@ -1461,7 +1459,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1473,7 +1471,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream password on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1600,7 +1598,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "dynamic clients are not allowed to use LDAP CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1613,7 +1611,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "dynamic clients are not allowed to use Active Directory CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1674,7 +1672,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1687,7 +1685,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1725,7 +1723,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1734,7 +1732,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1790,7 +1788,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1802,7 +1800,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -1812,7 +1810,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ @@ -1827,7 +1825,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1839,7 +1837,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -1849,7 +1847,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ @@ -1936,7 +1934,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1948,7 +1946,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client using Active Directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2003,7 +2001,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2015,7 +2013,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2025,7 +2023,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2036,7 +2034,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2048,7 +2046,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2058,7 +2056,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2094,7 +2092,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing client id in request using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), wantStatus: http.StatusUnauthorized, @@ -2148,7 +2146,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2206,7 +2204,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "invalid value for PKCE code_challenge_method in request using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2264,7 +2262,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "when PKCE code_challenge_method in request is `plain` using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2322,7 +2320,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge_method in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2388,7 +2386,7 @@ func TestAuthorizationEndpoint(t *testing.T) { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an LDAP upstream. name: "prompt param is not allowed to have none and another legal value at the same time using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2465,7 +2463,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}), @@ -2949,7 +2947,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream state does not have enough entropy using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -3049,7 +3047,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: multiple LDAP", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider, &upstreamLDAPIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider, upstreamLDAPIdentityProvider), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -3058,7 +3056,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: multiple Active Directory", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider, &upstreamLDAPIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamLDAPIdentityProvider, upstreamLDAPIdentityProvider), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -3067,7 +3065,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: both OIDC and LDAP", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(upstreamLDAPIdentityProvider), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -3076,7 +3074,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: OIDC, LDAP and AD", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(upstreamLDAPIdentityProvider).WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -3241,7 +3239,7 @@ func TestAuthorizationEndpoint(t *testing.T) { oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient, oidcClientsClient) oauthHelperWithNullStorage, _ := createOauthHelperWithNullStorage(secretsClient, oidcClientsClient) - idps := test.idps.Build() + idps := test.idps.BuildFederationDomainIdentityProvidersListerFinder() if len(test.wantDownstreamAdditionalClaims) > 0 { require.True(t, len(idps.GetOIDCIdentityProviders()) > 0, "wantDownstreamAdditionalClaims requires at least one OIDC IDP") } @@ -3268,7 +3266,7 @@ func TestAuthorizationEndpoint(t *testing.T) { oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace") oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient, oidcClientsClient) oauthHelperWithNullStorage, _ := createOauthHelperWithNullStorage(secretsClient, oidcClientsClient) - idpLister := test.idps.Build() + idpLister := test.idps.BuildFederationDomainIdentityProvidersListerFinder() subject := NewHandler( downstreamIssuer, idpLister, @@ -3287,7 +3285,7 @@ func TestAuthorizationEndpoint(t *testing.T) { WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}). WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123"}). Build() - idpLister.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{upstreamprovider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) + idpLister.SetOIDCIdentityProviders([]*oidctestutil.TestUpstreamOIDCIdentityProvider{newProviderSettings}) // Update the expectations of the test case to match the new upstream IDP settings. test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index a9f185c82..5670ef3f0 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -122,11 +122,11 @@ var ( func TestCallbackEndpoint(t *testing.T) { require.Len(t, happyDownstreamState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case") - otherUpstreamOIDCIdentityProvider := oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: "other-upstream-idp-name", - ClientID: "other-some-client-id", - Scopes: []string{"other-scope1", "other-scope2"}, - } + otherUpstreamOIDCIdentityProvider := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName("other-upstream-idp-name"). + WithClientID("other-some-client-id"). + WithScopes([]string{"other-scope1", "other-scope2"}). + Build() var stateEncoderHashKey = []byte("fake-hash-secret") var stateEncoderBlockKey = []byte("0123456789ABCDEF") // block encryption requires 16/24/32 bytes for AES @@ -1160,7 +1160,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "the OIDCIdentityProvider CRD has been deleted", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&otherUpstreamOIDCIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(otherUpstreamOIDCIdentityProvider), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -1457,7 +1457,7 @@ func TestCallbackEndpoint(t *testing.T) { jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) - subject := NewHandler(test.idps.Build(), oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI) + subject := NewHandler(test.idps.BuildFederationDomainIdentityProvidersListerFinder(), oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI) reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context") req := httptest.NewRequest(test.method, test.path, nil).WithContext(reqContext) if test.csrfCookie != "" { diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index 7ab52d07d..9a7fedf24 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -12,7 +12,6 @@ import ( "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil/oidctestutil" ) @@ -71,15 +70,15 @@ func TestIDPDiscovery(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "z-some-oidc-idp", AllowPasswordGrant: true}). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "x-some-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "a-some-ldap-idp"}). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "a-some-oidc-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). - WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ad-idp"}). - WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "y-some-ad-idp"}). - Build() + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("z-some-oidc-idp").WithAllowPasswordGrant(true).Build()). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-idp").Build()). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("a-some-ldap-idp").Build()). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("a-some-oidc-idp").Build()). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ldap-idp").Build()). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("x-some-idp").Build()). + WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ad-idp").Build()). + WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("y-some-ad-idp").Build()). + BuildFederationDomainIdentityProvidersListerFinder() handler := NewHandler(idpLister) req := httptest.NewRequest(test.method, test.path, nil) @@ -99,18 +98,17 @@ func TestIDPDiscovery(t *testing.T) { } // Change the list of IDPs in the cache. - idpLister.SetLDAPIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-1"}, - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-2"}, + idpLister.SetLDAPIdentityProviders([]*oidctestutil.TestUpstreamLDAPIdentityProvider{ + oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("some-other-ldap-idp-1").Build(), + oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("some-other-ldap-idp-2").Build(), }) - idpLister.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ - &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-1", AllowPasswordGrant: true}, - &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, + idpLister.SetOIDCIdentityProviders([]*oidctestutil.TestUpstreamOIDCIdentityProvider{ + oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("some-other-oidc-idp-1").WithAllowPasswordGrant(true).Build(), + oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("some-other-oidc-idp-2").Build(), }) - - idpLister.SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-2"}, - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-1"}, + idpLister.SetActiveDirectoryIdentityProviders([]*oidctestutil.TestUpstreamLDAPIdentityProvider{ + oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("some-other-ad-idp-2").Build(), + oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("some-other-ad-idp-1").Build(), }) // Make the same request to the same handler instance again, and expect different results. diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go index cd950adc6..0ac3fe511 100644 --- a/internal/oidc/login/post_login_handler_test.go +++ b/internal/oidc/login/post_login_handler_test.go @@ -171,27 +171,27 @@ func TestPostLoginEndpoint(t *testing.T) { return nil, false, nil } - upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: parsedUpstreamLDAPURL, - AuthenticateFunc: ldapAuthenticateFunc, - } + upstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc). + Build() - upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: parsedUpstreamLDAPURL, - AuthenticateFunc: ldapAuthenticateFunc, - } + upstreamActiveDirectoryIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc). + Build() - erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { + erroringUpstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithAuthenticateFunc(func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { return nil, false, fmt.Errorf("some ldap upstream auth error") - }, - } + }). + Build() expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{ Username: happyLDAPUsernameFromAuthenticator, @@ -290,8 +290,8 @@ func TestPostLoginEndpoint(t *testing.T) { { name: "happy LDAP login", idps: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one - WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), + WithLDAP(upstreamLDAPIdentityProvider). // should pick this one + WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -313,8 +313,8 @@ func TestPostLoginEndpoint(t *testing.T) { { name: "happy LDAP login with dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one - WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), + WithLDAP(upstreamLDAPIdentityProvider). // should pick this one + WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: happyLDAPDecodedStateForDynamicClient, formParams: happyUsernamePasswordFormParams, @@ -337,8 +337,8 @@ func TestPostLoginEndpoint(t *testing.T) { { name: "happy AD login", idps: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(&erroringUpstreamLDAPIdentityProvider). - WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one + WithLDAP(erroringUpstreamLDAPIdentityProvider). + WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), // should pick this one decodedState: happyActiveDirectoryDecodedState, formParams: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -360,8 +360,8 @@ func TestPostLoginEndpoint(t *testing.T) { { name: "happy AD login with dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(&erroringUpstreamLDAPIdentityProvider). - WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one + WithLDAP(erroringUpstreamLDAPIdentityProvider). + WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), // should pick this one kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: happyActiveDirectoryDecodedStateForDynamicClient, formParams: happyUsernamePasswordFormParams, @@ -383,7 +383,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_mode": "form_post"}, @@ -408,7 +408,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"}, @@ -433,7 +433,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -459,7 +459,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access pinniped:request-audience"}, @@ -485,7 +485,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is allowed to request username and groups but does not request them", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -511,7 +511,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request username and does not request username", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -545,7 +545,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request groups and does not request groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -579,7 +579,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{ @@ -610,8 +610,8 @@ func TestPostLoginEndpoint(t *testing.T) { { name: "happy LDAP login when username and groups scopes are not requested", idps: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one - WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), + WithLDAP(upstreamLDAPIdentityProvider). // should pick this one + WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid"}, @@ -637,7 +637,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "bad username LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{"wrong!"}, passParam: []string{happyLDAPPassword}}, wantStatus: http.StatusSeeOther, @@ -647,7 +647,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "bad password LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{"wrong!"}}, wantStatus: http.StatusSeeOther, @@ -657,7 +657,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "blank username LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{""}, passParam: []string{happyLDAPPassword}}, wantStatus: http.StatusSeeOther, @@ -667,7 +667,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "blank password LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{""}}, wantStatus: http.StatusSeeOther, @@ -677,7 +677,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "username and password sent as URI query params should be ignored since they are expected in form post body", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, reqURIQuery: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -687,7 +687,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "error during upstream LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -697,7 +697,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"}, @@ -708,7 +708,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -720,7 +720,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream client does not exist", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": "wrong_client_id"}, @@ -731,7 +731,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream client is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": ""}, @@ -742,7 +742,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type is unsupported", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_type": "unsupported"}, @@ -753,7 +753,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type form_post is unsupported for dynamic clients", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -765,7 +765,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_type": ""}, @@ -776,7 +776,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge": ""}, @@ -791,7 +791,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}, @@ -806,7 +806,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is `plain`", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": "plain"}, // plain is not allowed @@ -821,7 +821,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": ""}, @@ -836,7 +836,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is missing with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -852,7 +852,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "prompt param is not allowed to have none and another legal value at the same time", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"prompt": "none login"}, @@ -867,7 +867,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream state does not have enough entropy", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"state": "short"}, @@ -878,7 +878,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"}, @@ -889,7 +889,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -909,7 +909,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -929,7 +929,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -948,7 +948,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "upstream provider cannot be found by name and type", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type formParams: happyUsernamePasswordFormParams, wantErr: "error finding upstream provider: provider not found", @@ -988,7 +988,7 @@ func TestPostLoginEndpoint(t *testing.T) { rsp := httptest.NewRecorder() - subject := NewPostHandler(downstreamIssuer, tt.idps.Build(), oauthHelper) + subject := NewPostHandler(downstreamIssuer, tt.idps.BuildFederationDomainIdentityProvidersListerFinder(), oauthHelper) err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState) if tt.wantErr != "" { diff --git a/internal/oidc/provider/federation_domain_identity_providers_lister.go b/internal/oidc/provider/federation_domain_identity_providers_lister.go index ce333d3ca..855023c15 100644 --- a/internal/oidc/provider/federation_domain_identity_providers_lister.go +++ b/internal/oidc/provider/federation_domain_identity_providers_lister.go @@ -64,6 +64,11 @@ type FederationDomainIdentityProvidersListerI interface { GetActiveDirectoryIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider } +type FederationDomainIdentityProvidersListerFinderI interface { + FederationDomainIdentityProvidersListerI + FederationDomainIdentityProvidersFinderI +} + // FederationDomainIdentityProvidersLister wraps an UpstreamIdentityProvidersLister. The lister which is being // wrapped should contain all valid upstream providers that are currently defined in the Supervisor. // FederationDomainIdentityProvidersLister provides a lookup method which only looks up IDPs within those which diff --git a/internal/oidc/provider/federation_domain_issuer_test.go b/internal/oidc/provider/federation_domain_issuer_test.go index b80bff196..08a3d8514 100644 --- a/internal/oidc/provider/federation_domain_issuer_test.go +++ b/internal/oidc/provider/federation_domain_issuer_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/idtransform" ) func TestFederationDomainIssuerValidations(t *testing.T) { @@ -82,7 +84,7 @@ func TestFederationDomainIssuerValidations(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - _, err := NewFederationDomainIssuer(tt.issuer, nil) + _, err := NewFederationDomainIssuer(tt.issuer, []*FederationDomainIdentityProvider{}) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) } else { @@ -90,7 +92,11 @@ func TestFederationDomainIssuerValidations(t *testing.T) { } // This alternate constructor should perform all the same validations on the issuer string. - _, err = NewFederationDomainIssuerWithDefaultIDP(tt.issuer, nil) + _, err = NewFederationDomainIssuerWithDefaultIDP(tt.issuer, &FederationDomainIdentityProvider{ + DisplayName: "foobar", + UID: "foo-123", + Transforms: idtransform.NewTransformationPipeline(), + }) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) } else { diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 9f407b253..453192203 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -1,10 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package manager import ( - "context" "crypto/ecdsa" "encoding/json" "fmt" @@ -29,9 +28,6 @@ import ( "go.pinniped.dev/internal/secret" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" - "go.pinniped.dev/pkg/oidcclient/nonce" - "go.pinniped.dev/pkg/oidcclient/oidctypes" - "go.pinniped.dev/pkg/oidcclient/pkce" ) func TestManager(t *testing.T) { @@ -249,25 +245,19 @@ func TestManager(t *testing.T) { parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) r.NoError(err) - idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: upstreamIDPName, - ClientID: "test-client-id", - AuthorizationURL: *parsedUpstreamIDPAuthorizationURL, - Scopes: []string{"test-scope"}, - ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { - return &oidctypes.Token{ - IDToken: &oidctypes.IDToken{ - Claims: map[string]interface{}{ - "iss": "https://some-issuer.com", - "sub": "some-subject", - "username": "test-username", - "groups": "test-group1", - }, - }, - RefreshToken: &oidctypes.RefreshToken{Token: "some-opaque-token"}, - }, nil - }, - }).Build() + idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName(upstreamIDPName). + WithClientID("test-client-id"). + WithAuthorizationURL(*parsedUpstreamIDPAuthorizationURL). + WithScopes([]string{"test-scope"}). + WithIDTokenClaim("iss", "https://some-issuer.com"). + WithIDTokenClaim("sub", "some-subject"). + WithIDTokenClaim("username", "test-username"). + WithIDTokenClaim("groups", "test-group1"). + WithRefreshToken("some-opaque-token"). + WithoutAccessToken(). + Build(), + ).BuildDynamicUpstreamIDPProvider() kubeClient = fake.NewSimpleClientset() secretsClient := kubeClient.CoreV1().Secrets("some-namespace") @@ -387,9 +377,9 @@ func TestManager(t *testing.T) { when("given some valid providers via SetProviders()", func() { it.Before(func() { - p1, err := provider.NewFederationDomainIssuer(issuer1) + p1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - p2, err := provider.NewFederationDomainIssuer(issuer2) + p2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) subject.SetProviders(p1, p2) @@ -430,9 +420,9 @@ func TestManager(t *testing.T) { when("given the same valid providers as arguments to SetProviders() in reverse order", func() { it.Before(func() { - p1, err := provider.NewFederationDomainIssuer(issuer1) + p1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - p2, err := provider.NewFederationDomainIssuer(issuer2) + p2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) subject.SetProviders(p2, p1) diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index 4d2bf7081..09cb1596d 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -125,6 +125,7 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, } } +//nolint:funlen func upstreamOIDCRefresh( ctx context.Context, session *psession.PinnipedSession, @@ -201,7 +202,7 @@ func upstreamOIDCRefresh( var refreshedUntransformedGroups []string groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups) - if groupsScope { //nolint:nestif + if groupsScope { // If possible, update the user's group memberships. The configured groups claim name (if there is one) may or // may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the // claim name. It could also be missing because the claim was originally found in the ID token during login, but diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index bf463ef6b..494346d2f 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -881,7 +881,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) { // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build(), test.kubeResources) + test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) }) } } @@ -916,7 +916,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) { // First call - should be successful. // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. subject, rsp, authCode, _, secrets, oauthStore := exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build(), test.kubeResources) + test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) var parsedResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody)) @@ -1577,7 +1577,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build(), test.kubeResources) + test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) var parsedAuthcodeExchangeResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) @@ -2466,12 +2466,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships from LDAP, it updates groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"}, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups([]string{"new-group1", "new-group2", "new-group3"}). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ @@ -2493,12 +2494,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships from LDAP, it updates groups, using dynamic client - updates groups without outputting warnings", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"}, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups([]string{"new-group1", "new-group2", "new-group3"}). + Build(), + ), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, authcodeExchange: authcodeExchangeInputs{ customSessionData: happyLDAPCustomSessionData, @@ -2527,12 +2529,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns empty list of group memberships from LDAP, it updates groups to an empty list", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: []string{}, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups([]string{}). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ @@ -2553,12 +2556,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "ldap refresh grant when the upstream refresh when username and groups scopes are not requested on original request or refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"}, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups([]string{"new-group1", "new-group2", "new-group3"}). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, customSessionData: happyLDAPCustomSessionData, @@ -2694,12 +2698,13 @@ func TestRefreshGrant(t *testing.T) { // fosite does not look at the scopes provided in refresh requests, although it is a valid parameter. // even if 'groups' is not sent in the refresh request, we will send groups all the same. name: "refresh grant when the upstream refresh when groups scope requested on original request but not refresh refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"}, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups([]string{"new-group1", "new-group2", "new-group3"}). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyLDAPCustomSessionData, @@ -3469,12 +3474,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: goodGroups, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: happyRefreshTokenResponseForLDAP( @@ -3484,12 +3490,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: goodGroups, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + Build(), + ), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { @@ -3507,12 +3514,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path without downstream username scope granted, using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: goodGroups, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + Build(), + ), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { @@ -3549,12 +3557,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh happy path", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshGroups: goodGroups, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyActiveDirectoryCustomSessionData, @@ -3570,11 +3579,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh when the LDAP session data is nil", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: &psession.CustomSessionData{ @@ -3606,11 +3616,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh when the ad session data is nil", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: &psession.CustomSessionData{ @@ -3642,11 +3653,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh when the LDAP session data does not contain dn", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: &psession.CustomSessionData{ @@ -3682,11 +3694,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh when the active directory session data does not contain dn", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: &psession.CustomSessionData{ @@ -3722,12 +3735,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh returns an error", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshErr: errors.New("Some error performing upstream refresh"), - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshErr(errors.New("Some error performing upstream refresh")). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ @@ -3744,12 +3758,13 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh returns an error", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: activeDirectoryUpstreamResourceUID, - URL: ldapUpstreamURL, - PerformRefreshErr: errors.New("Some error performing upstream refresh"), - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshErr(errors.New("Some error performing upstream refresh")). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyActiveDirectoryCustomSessionData, @@ -3810,11 +3825,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "fosite session is empty", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, secrets v1.SecretInterface, refreshToken string) { refreshTokenSignature := getFositeDataSignature(t, refreshToken) @@ -3841,11 +3857,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "groups not found in extra field when the groups scope was granted", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyLDAPCustomSessionData, @@ -3878,11 +3895,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "username in custom session is empty string during refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyLDAPCustomSessionData, @@ -3915,11 +3933,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: "the-wrong-uid", - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID("the-wrong-uid"). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ @@ -3935,11 +3954,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the active directory provider in the session storage is found but has the wrong resource UID during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: activeDirectoryUpstreamName, - ResourceUID: "the-wrong-uid", - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID("the-wrong-uid"). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyActiveDirectoryCustomSessionData, @@ -3961,11 +3981,12 @@ func TestRefreshGrant(t *testing.T) { }, { name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: ldapUpstreamName, - ResourceUID: ldapUpstreamResourceUID, - URL: ldapUpstreamURL, - }), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + Build(), + ), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, secrets v1.SecretInterface, refreshToken string) { refreshTokenSignature := getFositeDataSignature(t, refreshToken) @@ -4002,7 +4023,7 @@ func TestRefreshGrant(t *testing.T) { // its actually fine to use this function even when simulating ldap (which uses a different flow) because it's // just populating a secret in storage. subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, - test.authcodeExchange, test.idps.Build(), test.kubeResources) + test.authcodeExchange, test.idps.BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) var parsedAuthcodeExchangeResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) @@ -4149,7 +4170,7 @@ func requireClaimsAreEqual(t *testing.T, claimName string, claimsOfTokenA map[st func exchangeAuthcodeForTokens( t *testing.T, test authcodeExchangeInputs, - idps provider.DynamicUpstreamIDPProvider, + idps provider.FederationDomainIdentityProvidersListerFinderI, kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset), ) ( subject http.Handler, diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 11b5c9885..5d67c2c30 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -296,7 +296,7 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, - plog.Logr(), //nolint:staticcheck // old controller with lots of log statements + plog.Logr(), //nolint:staticcheck // old controller with lots of log statements controllerlib.WithInformer, ), singletonWorker). diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index ee566c1f7..7f989b091 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -34,6 +34,7 @@ import ( "go.pinniped.dev/internal/fositestorage/openidconnect" pkce2 "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestoragei" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" @@ -97,15 +98,107 @@ type ValidateRefreshArgs struct { StoredAttributes upstreamprovider.RefreshAttributes } +func NewTestUpstreamLDAPIdentityProviderBuilder() *TestUpstreamLDAPIdentityProviderBuilder { + return &TestUpstreamLDAPIdentityProviderBuilder{} +} + +type TestUpstreamLDAPIdentityProviderBuilder struct { + name string + resourceUID types.UID + url *url.URL + authenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) + performRefreshCallCount int + performRefreshArgs []*PerformRefreshArgs + performRefreshErr error + performRefreshGroups []string + displayNameForFederationDomain string + transformsForFederationDomain *idtransform.TransformationPipeline +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithName(name string) *TestUpstreamLDAPIdentityProviderBuilder { + t.name = name + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithResourceUID(uid types.UID) *TestUpstreamLDAPIdentityProviderBuilder { + t.resourceUID = uid + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithURL(url *url.URL) *TestUpstreamLDAPIdentityProviderBuilder { + t.url = url + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithAuthenticateFunc(f func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)) *TestUpstreamLDAPIdentityProviderBuilder { + t.authenticateFunc = f + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshCallCount(count int) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshCallCount = count + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshArgs(args []*PerformRefreshArgs) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshArgs = args + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshErr(err error) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshErr = err + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshGroups(groups []string) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshGroups = groups + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamLDAPIdentityProviderBuilder { + t.displayNameForFederationDomain = displayName + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamLDAPIdentityProviderBuilder { + t.transformsForFederationDomain = transforms + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) Build() *TestUpstreamLDAPIdentityProvider { + if t.displayNameForFederationDomain == "" { + // default it to the CR name + t.displayNameForFederationDomain = t.name + } + if t.transformsForFederationDomain == nil { + // default to an empty pipeline + t.transformsForFederationDomain = idtransform.NewTransformationPipeline() + } + return &TestUpstreamLDAPIdentityProvider{ + Name: t.name, + ResourceUID: t.resourceUID, + URL: t.url, + AuthenticateFunc: t.authenticateFunc, + performRefreshCallCount: t.performRefreshCallCount, + performRefreshArgs: t.performRefreshArgs, + PerformRefreshErr: t.performRefreshErr, + PerformRefreshGroups: t.performRefreshGroups, + DisplayNameForFederationDomain: t.displayNameForFederationDomain, + TransformsForFederationDomain: t.transformsForFederationDomain, + } +} + type TestUpstreamLDAPIdentityProvider struct { - Name string - ResourceUID types.UID - URL *url.URL - AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) - performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs - PerformRefreshErr error - PerformRefreshGroups []string + Name string + ResourceUID types.UID + URL *url.URL + AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) + performRefreshCallCount int + performRefreshArgs []*PerformRefreshArgs + PerformRefreshErr error + PerformRefreshGroups []string + DisplayNameForFederationDomain string + TransformsForFederationDomain *idtransform.TransformationPipeline } var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} @@ -155,18 +248,20 @@ func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *Perform } type TestUpstreamOIDCIdentityProvider struct { - Name string - ClientID string - ResourceUID types.UID - AuthorizationURL url.URL - UserInfoURL bool - RevocationURL *url.URL - UsernameClaim string - GroupsClaim string - Scopes []string - AdditionalAuthcodeParams map[string]string - AdditionalClaimMappings map[string]string - AllowPasswordGrant bool + Name string + ClientID string + ResourceUID types.UID + AuthorizationURL url.URL + UserInfoURL bool + RevocationURL *url.URL + UsernameClaim string + GroupsClaim string + Scopes []string + AdditionalAuthcodeParams map[string]string + AdditionalClaimMappings map[string]string + AllowPasswordGrant bool + DisplayNameForFederationDomain string + TransformsForFederationDomain *idtransform.TransformationPipeline ExchangeAuthcodeAndValidateTokensFunc func( ctx context.Context, @@ -364,6 +459,104 @@ func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoArgs return u.validateTokenAndMergeWithUserInfoArgs[call] } +type TestFederationDomainIdentityProvidersListerFinder struct { + upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider + upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider + upstreamActiveDirectoryIdentityProviders []*TestUpstreamLDAPIdentityProvider +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*provider.FederationDomainResolvedOIDCIdentityProvider { + fdIDPs := make([]*provider.FederationDomainResolvedOIDCIdentityProvider, len(t.upstreamOIDCIdentityProviders)) + for i, testIDP := range t.upstreamOIDCIdentityProviders { + fdIDP := &provider.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeOIDC, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + } + return fdIDPs +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*provider.FederationDomainResolvedLDAPIdentityProvider { + fdIDPs := make([]*provider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamLDAPIdentityProviders)) + for i, testIDP := range t.upstreamLDAPIdentityProviders { + fdIDP := &provider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeLDAP, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + } + return fdIDPs +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*provider.FederationDomainResolvedLDAPIdentityProvider { + fdIDPs := make([]*provider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamActiveDirectoryIdentityProviders)) + for i, testIDP := range t.upstreamActiveDirectoryIdentityProviders { + fdIDP := &provider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeActiveDirectory, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + } + return fdIDPs +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) FindDefaultIDP() (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { + return nil, nil, fmt.Errorf("TODO: implement me") // TODO +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { + for _, testIDP := range t.upstreamOIDCIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return &provider.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeOIDC, + Transforms: testIDP.TransformsForFederationDomain, + }, nil, nil + } + } + for _, testIDP := range t.upstreamLDAPIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return nil, &provider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeLDAP, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } + for _, testIDP := range t.upstreamActiveDirectoryIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return nil, &provider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeActiveDirectory, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } + return nil, nil, fmt.Errorf("did not find IDP with name %q", upstreamIDPDisplayName) +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetOIDCIdentityProviders(providers []*TestUpstreamOIDCIdentityProvider) { + t.upstreamOIDCIdentityProviders = providers +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetLDAPIdentityProviders(providers []*TestUpstreamLDAPIdentityProvider) { + t.upstreamLDAPIdentityProviders = providers +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetActiveDirectoryIdentityProviders(providers []*TestUpstreamLDAPIdentityProvider) { + t.upstreamActiveDirectoryIdentityProviders = providers +} + type UpstreamIDPListerBuilder struct { upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider @@ -385,7 +578,15 @@ func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryId return b } -func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { +func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerFinder() *TestFederationDomainIdentityProvidersListerFinder { + return &TestFederationDomainIdentityProvidersListerFinder{ + upstreamOIDCIdentityProviders: b.upstreamOIDCIdentityProviders, + upstreamLDAPIdentityProviders: b.upstreamLDAPIdentityProviders, + upstreamActiveDirectoryIdentityProviders: b.upstreamActiveDirectoryIdentityProviders, + } +} + +func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() provider.DynamicUpstreamIDPProvider { idpProvider := provider.NewDynamicUpstreamIDPProvider() oidcUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) @@ -643,6 +844,8 @@ type TestUpstreamOIDCIdentityProviderBuilder struct { performRefreshErr error revokeTokenErr error validateTokenAndMergeWithUserInfoErr error + displayNameForFederationDomain string + transformsForFederationDomain *idtransform.TransformationPipeline } func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { @@ -792,19 +995,40 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRevokeTokenError(err error return u } +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamOIDCIdentityProviderBuilder { + u.displayNameForFederationDomain = displayName + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamOIDCIdentityProviderBuilder { + u.transformsForFederationDomain = transforms + return u +} + func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { + if u.displayNameForFederationDomain == "" { + // default it to the CR name + u.displayNameForFederationDomain = u.name + } + if u.transformsForFederationDomain == nil { + // default to an empty pipeline + u.transformsForFederationDomain = idtransform.NewTransformationPipeline() + } + return &TestUpstreamOIDCIdentityProvider{ - Name: u.name, - ClientID: u.clientID, - ResourceUID: u.resourceUID, - UsernameClaim: u.usernameClaim, - GroupsClaim: u.groupsClaim, - Scopes: u.scopes, - AllowPasswordGrant: u.allowPasswordGrant, - AuthorizationURL: u.authorizationURL, - UserInfoURL: u.hasUserInfoURL, - AdditionalAuthcodeParams: u.additionalAuthcodeParams, - AdditionalClaimMappings: u.additionalClaimMappings, + Name: u.name, + ClientID: u.clientID, + ResourceUID: u.resourceUID, + UsernameClaim: u.usernameClaim, + GroupsClaim: u.groupsClaim, + Scopes: u.scopes, + AllowPasswordGrant: u.allowPasswordGrant, + AuthorizationURL: u.authorizationURL, + UserInfoURL: u.hasUserInfoURL, + AdditionalAuthcodeParams: u.additionalAuthcodeParams, + AdditionalClaimMappings: u.additionalClaimMappings, + DisplayNameForFederationDomain: u.displayNameForFederationDomain, + TransformsForFederationDomain: u.transformsForFederationDomain, ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { if u.authcodeExchangeErr != nil { return nil, u.authcodeExchangeErr diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index e0b08adc1..871d60141 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -2338,7 +2338,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { state: state.State("test-state"), pkce: pkce.Code("test-pkce"), nonce: nonce.Nonce("test-nonce"), - logger: plog.Logr(), //nolint:staticcheck // old test with no log assertions + logger: plog.Logr(), //nolint:staticcheck // old test with no log assertions issuer: "https://valid-issuer.com/with/some/path", } if tt.opt != nil { From 5c0425fb71b0452265d08c83776146b379fb7396 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Tue, 13 Jun 2023 17:20:39 -0400 Subject: [PATCH 08/81] refactor: rename "provider" to "federationdomain" when appropriate Co-authored-by: Ryan Richard --- .../federation_domain_watcher.go | 18 +-- .../federation_domain_watcher_test.go | 110 +++++++++--------- internal/oidc/provider/manager/manager.go | 4 +- .../oidc/provider/manager/manager_test.go | 18 +-- 4 files changed, 75 insertions(+), 75 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 813667468..0663469fe 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -30,17 +30,17 @@ import ( "go.pinniped.dev/internal/plog" ) -// ProvidersSetter can be notified of all known valid providers with its SetIssuer function. +// FederationDomainsSetter can be notified of all known valid providers with its SetIssuer function. // If there are no longer any valid issuers, then it can be called with no arguments. // Implementations of this type should be thread-safe to support calls from multiple goroutines. -type ProvidersSetter interface { - SetProviders(federationDomains ...*provider.FederationDomainIssuer) +type FederationDomainsSetter interface { + SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) } type federationDomainWatcherController struct { - providerSetter ProvidersSetter - clock clock.Clock - client pinnipedclientset.Interface + federationDomainsSetter FederationDomainsSetter + clock clock.Clock + client pinnipedclientset.Interface federationDomainInformer configinformers.FederationDomainInformer oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer @@ -51,7 +51,7 @@ type federationDomainWatcherController struct { // NewFederationDomainWatcherController creates a controllerlib.Controller that watches // FederationDomain objects and notifies a callback object of the collection of provider configs. func NewFederationDomainWatcherController( - providerSetter ProvidersSetter, + federationDomainsSetter FederationDomainsSetter, clock clock.Clock, client pinnipedclientset.Interface, federationDomainInformer configinformers.FederationDomainInformer, @@ -64,7 +64,7 @@ func NewFederationDomainWatcherController( controllerlib.Config{ Name: "FederationDomainWatcherController", Syncer: &federationDomainWatcherController{ - providerSetter: providerSetter, + federationDomainsSetter: federationDomainsSetter, clock: clock, client: client, federationDomainInformer: federationDomainInformer, @@ -438,7 +438,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) } - c.providerSetter.SetProviders(federationDomainIssuers...) + c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) return errors.NewAggregate(errs) } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 980ecac69..66eb6e008 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -89,13 +89,13 @@ func TestInformerFilters(t *testing.T) { }, spec.Parallel(), spec.Report(report.Terminal{})) } -type fakeProvidersSetter struct { - SetProvidersWasCalled bool - FederationDomainsReceived []*provider.FederationDomainIssuer +type fakeFederationDomainsSetter struct { + SetFederationDomainsWasCalled bool + FederationDomainsReceived []*provider.FederationDomainIssuer } -func (f *fakeProvidersSetter) SetProviders(federationDomains ...*provider.FederationDomainIssuer) { - f.SetProvidersWasCalled = true +func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) { + f.SetFederationDomainsWasCalled = true f.FederationDomainsReceived = federationDomains } @@ -113,7 +113,7 @@ func TestSync(t *testing.T) { var cancelContextCancelFunc context.CancelFunc var syncContext *controllerlib.Context var frozenNow time.Time - var providersSetter *fakeProvidersSetter + var federationDomainsSetter *fakeFederationDomainsSetter var federationDomainGVR schema.GroupVersionResource // Defer starting the informers until the last possible moment so that the @@ -121,7 +121,7 @@ func TestSync(t *testing.T) { var startInformersAndController = func() { // Set this at the last second to allow for injection of server override. subject = NewFederationDomainWatcherController( - providersSetter, + federationDomainsSetter, clocktesting.NewFakeClock(frozenNow), pinnipedAPIClient, pinnipedInformers.Config().V1alpha1().FederationDomains(), @@ -149,7 +149,7 @@ func TestSync(t *testing.T) { it.Before(func() { r = require.New(t) - providersSetter = &fakeProvidersSetter{} + federationDomainsSetter = &fakeFederationDomainsSetter{} frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background()) @@ -191,24 +191,24 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain2)) }) - it("calls the ProvidersSetter", func() { + it("calls the FederationDomainsSetter", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.ElementsMatch( []*provider.FederationDomainIssuer{ - provider1, - provider2, + fd1, + fd2, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) @@ -292,24 +292,24 @@ func TestSync(t *testing.T) { r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) }) - it("calls the ProvidersSetter with both FederationDomain's", func() { + it("calls the FederationDomainsSetter with both FederationDomain's", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.ElementsMatch( []*provider.FederationDomainIssuer{ - provider1, - provider2, + fd1, + fd2, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) }) @@ -330,22 +330,22 @@ func TestSync(t *testing.T) { ) }) - it("sets the provider that it could actually update in the API", func() { + it("sets the FederationDomain that it could actually update in the API", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - provider1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - provider2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) - r.Len(providersSetter.FederationDomainsReceived, 1) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) + r.Len(federationDomainsSetter.FederationDomainsReceived, 1) r.True( - reflect.DeepEqual(providersSetter.FederationDomainsReceived[0], provider1) || - reflect.DeepEqual(providersSetter.FederationDomainsReceived[0], provider2), + reflect.DeepEqual(federationDomainsSetter.FederationDomainsReceived[0], fd1) || + reflect.DeepEqual(federationDomainsSetter.FederationDomainsReceived[0], fd2), ) }) @@ -549,20 +549,20 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(invalidFederationDomain)) }) - it("calls the ProvidersSetter with the valid provider", func() { + it("calls the FederationDomainsSetter with the valid FederationDomain", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + validFederationDomain, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( []*provider.FederationDomainIssuer{ - validProvider, + validFederationDomain, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) @@ -623,20 +623,20 @@ func TestSync(t *testing.T) { ) }) - it("sets the provider that it could actually update in the API", func() { + it("sets the FederationDomain that it could actually update in the API", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - validProvider, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + validFederationDomain, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( []*provider.FederationDomainIssuer{ - validProvider, + validFederationDomain, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) @@ -713,20 +713,20 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) }) - it("calls the ProvidersSetter with the non-duplicate", func() { + it("calls the FederationDomainsSetter with the non-duplicate", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + nonDuplicateFederationDomain, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( []*provider.FederationDomainIssuer{ - nonDuplicateProvider, + nonDuplicateFederationDomain, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) @@ -840,7 +840,7 @@ func TestSync(t *testing.T) { it.Before(func() { federationDomainSameIssuerAddress1 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "provider1", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{ Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -849,7 +849,7 @@ func TestSync(t *testing.T) { r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) federationDomainSameIssuerAddress2 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "provider2", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{ // Validation treats these as the same DNS hostname even though they have different port numbers, // because SNI information on the incoming requests is not going to include port numbers. @@ -861,7 +861,7 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) federationDomainDifferentIssuerAddress = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressProvider", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{ Issuer: "https://issuer-not-duplicate.com", TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -876,7 +876,7 @@ func TestSync(t *testing.T) { _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. r.Error(err) federationDomainWithInvalidIssuerURL = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLProvider", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace}, Spec: v1alpha1.FederationDomainSpec{ Issuer: invalidIssuerURL, TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -886,20 +886,20 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) }) - it("calls the ProvidersSetter with the non-duplicate", func() { + it("calls the FederationDomainsSetter with the non-duplicate", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateProvider, err := provider.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + nonDuplicateFederationDomain, err := provider.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - r.True(providersSetter.SetProvidersWasCalled) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( []*provider.FederationDomainIssuer{ - nonDuplicateProvider, + nonDuplicateFederationDomain, }, - providersSetter.FederationDomainsReceived, + federationDomainsSetter.FederationDomainsReceived, ) }) @@ -1029,8 +1029,8 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(pinnipedAPIClient.Actions()) - r.True(providersSetter.SetProvidersWasCalled) - r.Empty(providersSetter.FederationDomainsReceived) + r.True(federationDomainsSetter.SetFederationDomainsWasCalled) + r.Empty(federationDomainsSetter.FederationDomainsReceived) }) }) }, spec.Parallel(), spec.Report(report.Terminal{})) diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 5793765d7..2c8df7a0a 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -69,7 +69,7 @@ func NewManager( } } -// SetProviders adds or updates all the given providerHandlers using each provider's issuer string +// SetFederationDomains adds or updates all the given providerHandlers using each provider's issuer string // as the name of the provider to decide if it is an add or update operation. // // It also removes any providerHandlers that were previously added but were not passed in to @@ -77,7 +77,7 @@ func NewManager( // // This method assumes that all of the FederationDomainIssuer arguments have already been validated // by someone else before they are passed to this method. -func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIssuer) { +func (m *Manager) SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) { m.mu.Lock() defer m.mu.Unlock() diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 453192203..8cff064df 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -277,7 +277,7 @@ func TestManager(t *testing.T) { subject = NewManager(nextHandler, dynamicJWKSProvider, idpLister, &cache, secretsClient, oidcClientsClient) }) - when("given no providers via SetProviders()", func() { + when("given no providers via SetFederationDomains()", func() { it("sends all requests to the nextHandler", func() { r.False(fallbackHandlerWasCalled) subject.ServeHTTP(httptest.NewRecorder(), newGetRequest("/anything")) @@ -375,13 +375,13 @@ func TestManager(t *testing.T) { requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode4, issuer2JWKS, issuer2) } - when("given some valid providers via SetProviders()", func() { + when("given some valid providers via SetFederationDomains()", func() { it.Before(func() { - p1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - p2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - subject.SetProviders(p1, p2) + subject.SetFederationDomains(fd1, fd2) jwksMap := map[string]*jose.JSONWebKeySet{ issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}}, @@ -418,13 +418,13 @@ func TestManager(t *testing.T) { }) }) - when("given the same valid providers as arguments to SetProviders() in reverse order", func() { + when("given the same valid providers as arguments to SetFederationDomains() in reverse order", func() { it.Before(func() { - p1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - p2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) r.NoError(err) - subject.SetProviders(p2, p1) + subject.SetFederationDomains(fd2, fd1) jwksMap := map[string]*jose.JSONWebKeySet{ issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}}, From 8f6a12eae455a0c9ef732abb308dd286464ebba1 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Wed, 14 Jun 2023 16:31:57 -0400 Subject: [PATCH 09/81] fix internal/oidc/provider/manager/manager_test.go Co-authored-by: Ryan Richard --- .../oidc/provider/manager/manager_test.go | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 8cff064df..9f02d9cb4 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -21,6 +21,7 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/jwks" @@ -38,6 +39,7 @@ func TestManager(t *testing.T) { nextHandler http.HandlerFunc fallbackHandlerWasCalled bool dynamicJWKSProvider jwks.DynamicJWKSProvider + federationDomainIDPs []*provider.FederationDomainIdentityProvider kubeClient *fake.Clientset ) @@ -50,6 +52,7 @@ func TestManager(t *testing.T) { issuer2KeyID = "issuer2-key" upstreamIDPAuthorizationURL = "https://test-upstream.com/auth" upstreamIDPName = "test-idp" + upstreamResourceUID = "test-resource-uid" upstreamIDPType = "oidc" downstreamClientID = "pinniped-cli" downstreamRedirectURL = "http://127.0.0.1:12345/callback" @@ -245,9 +248,19 @@ func TestManager(t *testing.T) { parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) r.NoError(err) + + federationDomainIDPs = []*provider.FederationDomainIdentityProvider{ + { + DisplayName: upstreamIDPName, + UID: upstreamResourceUID, + Transforms: idtransform.NewTransformationPipeline(), + }, + } + idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(upstreamIDPName). WithClientID("test-client-id"). + WithResourceUID(upstreamResourceUID). WithAuthorizationURL(*parsedUpstreamIDPAuthorizationURL). WithScopes([]string{"test-scope"}). WithIDTokenClaim("iss", "https://some-issuer.com"). @@ -332,6 +345,7 @@ func TestManager(t *testing.T) { requireJWKSRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2KeyID) authRequestParams := "?" + url.Values{ + "pinniped_idp_name": []string{upstreamIDPName}, "response_type": []string{"code"}, "scope": []string{"openid profile email username groups"}, "client_id": []string{downstreamClientID}, @@ -377,9 +391,9 @@ func TestManager(t *testing.T) { when("given some valid providers via SetFederationDomains()", func() { it.Before(func() { - fd1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(issuer1, federationDomainIDPs) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(issuer2, federationDomainIDPs) r.NoError(err) subject.SetFederationDomains(fd1, fd2) @@ -420,9 +434,9 @@ func TestManager(t *testing.T) { when("given the same valid providers as arguments to SetFederationDomains() in reverse order", func() { it.Before(func() { - fd1, err := provider.NewFederationDomainIssuer(issuer1, []*provider.FederationDomainIdentityProvider{}) + fd1, err := provider.NewFederationDomainIssuer(issuer1, federationDomainIDPs) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(issuer2, []*provider.FederationDomainIdentityProvider{}) + fd2, err := provider.NewFederationDomainIssuer(issuer2, federationDomainIDPs) r.NoError(err) subject.SetFederationDomains(fd2, fd1) From 793d1c6a5d2d57f6816beb4c083ca5de64c1f8f4 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Jun 2023 10:33:06 -0700 Subject: [PATCH 10/81] add a type assertion --- .../provider/federation_domain_identity_providers_lister.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/oidc/provider/federation_domain_identity_providers_lister.go b/internal/oidc/provider/federation_domain_identity_providers_lister.go index 855023c15..42d47ed3c 100644 --- a/internal/oidc/provider/federation_domain_identity_providers_lister.go +++ b/internal/oidc/provider/federation_domain_identity_providers_lister.go @@ -69,6 +69,9 @@ type FederationDomainIdentityProvidersListerFinderI interface { FederationDomainIdentityProvidersFinderI } +// FederationDomainIdentityProvidersLister implements FederationDomainIdentityProvidersListerFinderI. +var _ FederationDomainIdentityProvidersListerFinderI = (*FederationDomainIdentityProvidersLister)(nil) + // FederationDomainIdentityProvidersLister wraps an UpstreamIdentityProvidersLister. The lister which is being // wrapped should contain all valid upstream providers that are currently defined in the Supervisor. // FederationDomainIdentityProvidersLister provides a lookup method which only looks up IDPs within those which From 6ef9cf273eb44d0d240a8e000c3a99e58b3f9c1e Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Thu, 15 Jun 2023 16:26:51 -0400 Subject: [PATCH 11/81] Fix post_login_handler_test.go Co-authored-by: Ryan Richard --- .../oidc/login/post_login_handler_test.go | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go index 0ac3fe511..be4482189 100644 --- a/internal/oidc/login/post_login_handler_test.go +++ b/internal/oidc/login/post_login_handler_test.go @@ -194,12 +194,14 @@ func TestPostLoginEndpoint(t *testing.T) { Build() expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{ - Username: happyLDAPUsernameFromAuthenticator, - ProviderUID: activeDirectoryUpstreamResourceUID, - ProviderName: activeDirectoryUpstreamName, - ProviderType: psession.ProviderTypeActiveDirectory, - OIDC: nil, - LDAP: nil, + Username: happyLDAPUsernameFromAuthenticator, + ProviderUID: activeDirectoryUpstreamResourceUID, + ProviderName: activeDirectoryUpstreamName, + ProviderType: psession.ProviderTypeActiveDirectory, + UpstreamUsername: happyLDAPUsernameFromAuthenticator, + UpstreamGroups: happyLDAPGroups, + OIDC: nil, + LDAP: nil, ActiveDirectory: &psession.ActiveDirectorySessionData{ UserDN: happyLDAPUserDN, ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, @@ -207,11 +209,13 @@ func TestPostLoginEndpoint(t *testing.T) { } expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{ - Username: happyLDAPUsernameFromAuthenticator, - ProviderUID: ldapUpstreamResourceUID, - ProviderName: ldapUpstreamName, - ProviderType: psession.ProviderTypeLDAP, - OIDC: nil, + Username: happyLDAPUsernameFromAuthenticator, + ProviderUID: ldapUpstreamResourceUID, + ProviderName: ldapUpstreamName, + ProviderType: psession.ProviderTypeLDAP, + UpstreamUsername: happyLDAPUsernameFromAuthenticator, + UpstreamGroups: happyLDAPGroups, + OIDC: nil, LDAP: &psession.LDAPSessionData{ UserDN: happyLDAPUserDN, ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, @@ -944,14 +948,14 @@ func TestPostLoginEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder(), // empty decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, - wantErr: "error finding upstream provider: provider not found", + wantErr: "error finding upstream provider: did not find IDP with name \"some-ldap-idp\"", }, { name: "upstream provider cannot be found by name and type", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type formParams: happyUsernamePasswordFormParams, - wantErr: "error finding upstream provider: provider not found", + wantErr: "error finding upstream provider: did not find IDP with name \"some-active-directory-idp\"", }, } From 770f8af62bd55d9c926d55dca43cea02cf942dcd Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Thu, 15 Jun 2023 17:02:07 -0400 Subject: [PATCH 12/81] Update auth_handler.go to return 422 error when upstream IdP not found Co-authored-by: Ryan Richard --- internal/oidc/auth/auth_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 7b94694fc..10e486bb2 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -63,7 +63,7 @@ func NewHandler( oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpNameQueryParamValue, idpFinder) if err != nil { plog.WarningErr("authorize upstream config", err) - return err + return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } if oidcUpstream != nil { From 610f886fd84f81dccb9feb4a59c23c6f08ed1c12 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 16 Jun 2023 14:48:01 -0700 Subject: [PATCH 13/81] Fix auth_handler_test.go --- internal/oidc/auth/auth_handler.go | 60 +- internal/oidc/auth/auth_handler_test.go | 554 ++++++++++-------- .../testutil/oidctestutil/oidctestutil.go | 10 +- 3 files changed, 364 insertions(+), 260 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 10e486bb2..6bb7ea092 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -56,23 +56,54 @@ func NewHandler( return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) } + requestedBrowserlessFlow := len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 || + len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 + + // Need to parse the request params so we can get the IDP name. The style and text of the error is inspired by + // fosite's implementation of NewAuthorizeRequest(). Fosite only calls ParseMultipartForm() there. However, + // although ParseMultipartForm() calls ParseForm(), it swallows errors from ParseForm() sometimes. To avoid + // having any errors swallowed, we call both. When fosite calls ParseMultipartForm() later, it will be a noop. + if err := r.ParseForm(); err != nil { + oidc.WriteAuthorizeError(r, w, + oauthHelperWithoutStorage, + fosite.NewAuthorizeRequest(), + fosite.ErrInvalidRequest. + WithHint("Unable to parse form params, make sure to send a properly formatted query params or form request body."). + WithWrap(err).WithDebug(err.Error()), + requestedBrowserlessFlow) + return nil + } + if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + oidc.WriteAuthorizeError(r, w, + oauthHelperWithoutStorage, + fosite.NewAuthorizeRequest(), + fosite.ErrInvalidRequest. + WithHint("Unable to parse multipart HTTP body, make sure to send a properly formatted form request body."). + WithWrap(err).WithDebug(err.Error()), + requestedBrowserlessFlow) + return nil + } + // Note that the client might have used oidcapi.AuthorizeUpstreamIDPNameParamName and - // oidcapi.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP. + // oidcapi.AuthorizeUpstreamIDPTypeParamName query (or form) params to request a certain upstream IDP. // The Pinniped CLI has been sending these params since v0.9.0. - idpNameQueryParamValue := r.URL.Query().Get(oidcapi.AuthorizeUpstreamIDPNameParamName) + idpNameQueryParamValue := r.Form.Get(oidcapi.AuthorizeUpstreamIDPNameParamName) oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpNameQueryParamValue, idpFinder) if err != nil { - plog.WarningErr("authorize upstream config", err) - return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + oidc.WriteAuthorizeError(r, w, + oauthHelperWithoutStorage, + fosite.NewAuthorizeRequest(), + fosite.ErrInvalidRequest. + WithHintf("%q param error: %s", oidcapi.AuthorizeUpstreamIDPNameParamName, err.Error()). + WithWrap(err).WithDebug(err.Error()), + requestedBrowserlessFlow) + return nil } if oidcUpstream != nil { - if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 || - len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 { + if requestedBrowserlessFlow { // The client set a username header, so they are trying to log in with a username/password. - return handleAuthRequestForOIDCUpstreamPasswordGrant( - r, - w, + return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream.Provider, oidcUpstream.Transforms, @@ -91,8 +122,7 @@ func NewHandler( } // We know it's an AD/LDAP upstream. - if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 || - len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 { + if requestedBrowserlessFlow { // The client set a username header, so they are trying to log in with a username/password. return handleAuthRequestForLDAPUpstreamCLIFlow(r, w, oauthHelperWithStorage, @@ -102,13 +132,9 @@ func NewHandler( idpNameQueryParamValue, ) } - return handleAuthRequestForLDAPUpstreamBrowserFlow( - r, - w, + return handleAuthRequestForLDAPUpstreamBrowserFlow(r, w, oauthHelperWithoutStorage, - generateCSRF, - generateNonce, - generatePKCE, + generateCSRF, generateNonce, generatePKCE, ldapUpstream, ldapUpstream.SessionProviderType, downstreamIssuer, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index f7ff3a88b..5a6a1087a 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -71,6 +71,7 @@ func TestAuthorizationEndpoint(t *testing.T) { downstreamPKCEChallengeMethod = "S256" happyState = "8b-state" upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" + plainContentType = "text/plain; charset=utf-8" htmlContentType = "text/html; charset=utf-8" jsonContentType = "application/json; charset=utf-8" formContentType = "application/x-www-form-urlencoded" @@ -406,26 +407,67 @@ func TestAuthorizationEndpoint(t *testing.T) { happyGetRequestPath := pathWithQuery("/some/path", happyGetRequestQueryMap) - modifiedHappyGetRequestQueryMap := func(queryOverrides map[string]string) map[string]string { - copyOfHappyGetRequestQueryMap := map[string]string{} - for k, v := range happyGetRequestQueryMap { - copyOfHappyGetRequestQueryMap[k] = v + modifiedQueryMap := func(modifyThisMap map[string]string, queryOverrides map[string]string) map[string]string { + copyModifyThisMap := map[string]string{} + for k, v := range modifyThisMap { + copyModifyThisMap[k] = v } for k, v := range queryOverrides { - _, hasKey := copyOfHappyGetRequestQueryMap[k] + _, hasKey := copyModifyThisMap[k] if v == "" && hasKey { - delete(copyOfHappyGetRequestQueryMap, k) + delete(copyModifyThisMap, k) } else { - copyOfHappyGetRequestQueryMap[k] = v + copyModifyThisMap[k] = v } } - return copyOfHappyGetRequestQueryMap + return copyModifyThisMap + } + + modifiedHappyGetRequestQueryMap := func(queryOverrides map[string]string) map[string]string { + return modifiedQueryMap(happyGetRequestQueryMap, queryOverrides) } modifiedHappyGetRequestPath := func(queryOverrides map[string]string) string { return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides)) } + happyGetRequestPathForOIDCUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": oidcUpstreamName}) + happyGetRequestPathForOIDCPasswordGrantUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": oidcPasswordGrantUpstreamName}) + happyGetRequestPathForLDAPUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": ldapUpstreamName}) + happyGetRequestPathForADUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": activeDirectoryUpstreamName}) + + modifiedHappyGetRequestPathForOIDCUpstream := func(queryOverrides map[string]string) string { + queryOverrides["pinniped_idp_name"] = oidcUpstreamName + return modifiedHappyGetRequestPath(queryOverrides) + } + modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream := func(queryOverrides map[string]string) string { + queryOverrides["pinniped_idp_name"] = oidcPasswordGrantUpstreamName + return modifiedHappyGetRequestPath(queryOverrides) + } + modifiedHappyGetRequestPathForLDAPUpstream := func(queryOverrides map[string]string) string { + queryOverrides["pinniped_idp_name"] = ldapUpstreamName + return modifiedHappyGetRequestPath(queryOverrides) + } + modifiedHappyGetRequestPathForADUpstream := func(queryOverrides map[string]string) string { + queryOverrides["pinniped_idp_name"] = activeDirectoryUpstreamName + return modifiedHappyGetRequestPath(queryOverrides) + } + + happyGetRequestQueryMapForOIDCUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": oidcUpstreamName}) + happyGetRequestQueryMapForOIDCPasswordGrantUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": oidcPasswordGrantUpstreamName}) + happyGetRequestQueryMapForLDAPUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": ldapUpstreamName}) + happyGetRequestQueryMapForADUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": activeDirectoryUpstreamName}) + + modifiedHappyGetRequestQueryMapForOIDCUpstream := func(queryOverrides map[string]string) map[string]string { + return modifiedQueryMap(happyGetRequestQueryMapForOIDCUpstream, queryOverrides) + } + modifiedHappyGetRequestQueryMapForLDAPUpstream := func(queryOverrides map[string]string) map[string]string { + return modifiedQueryMap(happyGetRequestQueryMapForLDAPUpstream, queryOverrides) + } + modifiedHappyGetRequestQueryMapForADUpstream := func(queryOverrides map[string]string) map[string]string { + return modifiedQueryMap(happyGetRequestQueryMapForADUpstream, queryOverrides) + } + expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamName, upstreamType string) string { csrf := happyCSRF if csrfValueOverride != "" { @@ -464,12 +506,14 @@ func TestAuthorizationEndpoint(t *testing.T) { } expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{ - Username: happyLDAPUsernameFromAuthenticator, - ProviderUID: activeDirectoryUpstreamResourceUID, - ProviderName: activeDirectoryUpstreamName, - ProviderType: psession.ProviderTypeActiveDirectory, - OIDC: nil, - LDAP: nil, + Username: happyLDAPUsernameFromAuthenticator, + UpstreamUsername: happyLDAPUsernameFromAuthenticator, + UpstreamGroups: happyLDAPGroups, + ProviderUID: activeDirectoryUpstreamResourceUID, + ProviderName: activeDirectoryUpstreamName, + ProviderType: psession.ProviderTypeActiveDirectory, + OIDC: nil, + LDAP: nil, ActiveDirectory: &psession.ActiveDirectorySessionData{ UserDN: happyLDAPUserDN, ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, @@ -477,11 +521,13 @@ func TestAuthorizationEndpoint(t *testing.T) { } expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{ - Username: happyLDAPUsernameFromAuthenticator, - ProviderUID: ldapUpstreamResourceUID, - ProviderName: ldapUpstreamName, - ProviderType: psession.ProviderTypeLDAP, - OIDC: nil, + Username: happyLDAPUsernameFromAuthenticator, + UpstreamUsername: happyLDAPUsernameFromAuthenticator, + UpstreamGroups: happyLDAPGroups, + ProviderUID: ldapUpstreamResourceUID, + ProviderName: ldapUpstreamName, + ProviderType: psession.ProviderTypeLDAP, + OIDC: nil, LDAP: &psession.LDAPSessionData{ UserDN: happyLDAPUserDN, ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, @@ -490,10 +536,12 @@ func TestAuthorizationEndpoint(t *testing.T) { } expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: oidcPasswordGrantUpstreamResourceUID, - ProviderName: oidcPasswordGrantUpstreamName, - ProviderType: psession.ProviderTypeOIDC, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: oidcPasswordGrantUpstreamResourceUID, + ProviderName: oidcPasswordGrantUpstreamName, + ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: oidcPasswordGrantUpstreamRefreshToken, UpstreamSubject: oidcUpstreamSubject, @@ -501,19 +549,23 @@ func TestAuthorizationEndpoint(t *testing.T) { }, } - expectedHappyOIDCPasswordGrantCustomSessionWithUsername := func(wantUsername string) *psession.CustomSessionData { + expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups := func(wantUsername string, wantGroups []string) *psession.CustomSessionData { copyOfCustomSession := *expectedHappyOIDCPasswordGrantCustomSession copyOfOIDC := *(expectedHappyOIDCPasswordGrantCustomSession.OIDC) copyOfCustomSession.OIDC = ©OfOIDC copyOfCustomSession.Username = wantUsername + copyOfCustomSession.UpstreamUsername = wantUsername + copyOfCustomSession.UpstreamGroups = wantGroups return ©OfCustomSession } expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken := &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: oidcPasswordGrantUpstreamResourceUID, - ProviderName: oidcPasswordGrantUpstreamName, - ProviderType: psession.ProviderTypeOIDC, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: oidcPasswordGrantUpstreamResourceUID, + ProviderName: oidcPasswordGrantUpstreamName, + ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, UpstreamSubject: oidcUpstreamSubject, @@ -592,7 +644,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCUpstream, wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -610,7 +662,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -627,7 +679,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -645,7 +697,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -662,7 +714,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -680,7 +732,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -692,7 +744,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "OIDC upstream password grant happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -722,7 +774,7 @@ func TestAuthorizationEndpoint(t *testing.T) { WithIDTokenClaim("upstreamOtherClaim", []interface{}{"hello", true}). Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -753,7 +805,7 @@ func TestAuthorizationEndpoint(t *testing.T) { WithIDTokenClaim("not-upstream", "value"). Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -776,7 +828,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "LDAP cli upstream happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -797,7 +849,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "ActiveDirectory cli upstream happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -823,7 +875,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCUpstream, csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -840,7 +892,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -857,7 +909,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -876,7 +928,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForOIDCUpstream), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -896,7 +948,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), + body: encodeQuery(modifiedHappyGetRequestQueryMapForOIDCUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -915,7 +967,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForLDAPUpstream), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -935,7 +987,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), + body: encodeQuery(modifiedHappyGetRequestQueryMapForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -954,7 +1006,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForADUpstream), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -974,7 +1026,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), + body: encodeQuery(modifiedHappyGetRequestQueryMapForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})), wantStatus: http.StatusSeeOther, wantContentType: "", wantBodyString: "", @@ -988,7 +1040,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForOIDCPasswordGrantUpstream), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1012,7 +1064,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForLDAPUpstream), customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1035,7 +1087,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodPost, path: "/some/path", contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), + body: encodeQuery(happyGetRequestQueryMapForADUpstream), customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1061,7 +1113,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"prompt": "login"}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, @@ -1078,7 +1130,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": "currently-ignored", "pinniped_idp_type": "oidc"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"pinniped_idp_type": "oidc"}), contentType: formContentType, wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -1096,7 +1148,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"prompt": "login"}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, @@ -1113,7 +1165,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"prompt": "none"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery), @@ -1128,7 +1180,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCUpstream, csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -1147,7 +1199,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }), wantStatus: http.StatusSeeOther, @@ -1169,7 +1221,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, @@ -1189,7 +1241,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "OIDC upstream password grant happy path when downstream redirect uri matches what is configured for client except for the port number", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1213,7 +1265,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1241,7 +1293,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid offline_access"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"scope": "openid offline_access"}), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, @@ -1255,7 +1307,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "OIDC password grant happy path when upstream IDP returned empty refresh token but it did return an access token and has a userinfo endpoint", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1277,7 +1329,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "OIDC password grant happy path when upstream IDP returned empty refresh token and an access token that has a short lifetime", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1294,11 +1346,13 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: oidcPasswordGrantUpstreamResourceUID, - ProviderName: oidcPasswordGrantUpstreamName, - ProviderType: psession.ProviderTypeOIDC, - Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."}, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: oidcPasswordGrantUpstreamResourceUID, + ProviderName: oidcPasswordGrantUpstreamName, + ProviderType: psession.ProviderTypeOIDC, + Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."}, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, UpstreamSubject: oidcUpstreamSubject, @@ -1310,7 +1364,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "OIDC password grant happy path when upstream IDP did not return a refresh token but it did return an access token and has a userinfo endpoint", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1332,7 +1386,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "error during upstream LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusBadGateway, @@ -1343,7 +1397,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "error during upstream Active Directory authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusBadGateway, @@ -1359,7 +1413,7 @@ func TestAuthorizationEndpoint(t *testing.T) { Build(), ), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To("wrong-password"), wantPasswordGrantCall: &expectedPasswordGrant{ @@ -1377,7 +1431,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "wrong upstream password for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To("wrong-password"), wantStatus: http.StatusFound, @@ -1389,7 +1443,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "wrong upstream password for Active Directory authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To("wrong-password"), wantStatus: http.StatusFound, @@ -1401,7 +1455,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "wrong upstream username for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To("wrong-username"), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1413,7 +1467,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "wrong upstream username for Active Directory authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To("wrong-username"), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1425,7 +1479,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream username but has password on request for OIDC password grant", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: nil, // do not send header customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1437,7 +1491,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream username but has password on request for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: nil, // do not send header customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1449,7 +1503,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream username on request for Active Directory authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, customUsernameHeader: nil, // do not send header customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1461,7 +1515,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream password on request for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, @@ -1473,7 +1527,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream password on request for Active Directory authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, @@ -1485,7 +1539,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns no refresh token with an access token but has no userinfo endpoint", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1498,7 +1552,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns empty refresh token with an access token but has no userinfo endpoint", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1511,7 +1565,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns empty refresh token and empty access token", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1524,7 +1578,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns no refresh and no access token", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1537,7 +1591,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns no refresh token and empty access token", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1550,7 +1604,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "password grant returns an error when upstream IDP returns empty refresh token and no access token", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, @@ -1563,7 +1617,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "missing upstream password on request for OIDC password grant authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, @@ -1575,7 +1629,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: happyGetRequestPath, + path: happyGetRequestPathForOIDCUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1588,7 +1642,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1601,7 +1655,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1614,7 +1668,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1631,7 +1685,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), wantStatus: http.StatusBadRequest, @@ -1648,7 +1702,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-dynamic-client", "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, @@ -1661,7 +1715,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream redirect uri does not match what is configured for client when using OIDC upstream password grant", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1674,7 +1728,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1687,7 +1741,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream redirect uri does not match what is configured for client when using active directory upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1705,7 +1759,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, @@ -1714,7 +1768,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream client does not exist when using OIDC upstream password grant", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"client_id": "invalid-client"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusUnauthorized, @@ -1725,7 +1779,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream client does not exist when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, @@ -1734,7 +1788,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream client does not exist when using active directory upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, @@ -1748,7 +1802,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), @@ -1764,7 +1818,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{ "response_type": "unsupported", "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, @@ -1778,7 +1832,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "response type is unsupported when using OIDC upstream password grant", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1790,7 +1844,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "response type is unsupported when using LDAP cli upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(happyLDAPUsername), customPasswordHeader: ptr.To(happyLDAPPassword), wantStatus: http.StatusFound, @@ -1802,7 +1856,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "response type is unsupported when using LDAP browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), @@ -1813,7 +1867,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "response_type": "unsupported", "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, @@ -1827,7 +1881,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "response type is unsupported when using active directory cli upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1839,7 +1893,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "response type is unsupported when using active directory browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), @@ -1850,7 +1904,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{ + path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ "response_type": "unsupported", "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, @@ -1869,7 +1923,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"scope": "openid profile email tuna"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), @@ -1885,7 +1939,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": "openid tuna"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"client_id": dynamicClientID, "scope": "openid tuna"}), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), @@ -1895,7 +1949,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name: "downstream scopes do not match what is configured for client using OIDC upstream password grant", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}), + path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"scope": "openid profile email tuna"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), customPasswordHeader: ptr.To(oidcUpstreamPassword), wantStatus: http.StatusFound, @@ -1912,7 +1966,7 @@ func TestAuthorizationEndpoint(t *testing.T) { stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, - path: modifiedHappyGetRequestPath(map[string]string{"response_mode": "form_post", "scope": "openid profile email tuna"}), + path: modifiedHappyGetRequestPathForOIDCUpstream(map[string]string{"response_mode": "form_post", "scope": "openid profile email tuna"}), wantStatus: http.StatusOK, wantContentType: htmlContentType, wantBodyRegex: ` Date: Thu, 22 Jun 2023 16:12:50 -0400 Subject: [PATCH 14/81] Reorganized FederationDomain packages to avoid circular dependency Co-authored-by: Ryan Richard --- .../dynamiccertauthority.go | 4 +- .../dynamiccertauthority_test.go | 4 +- .../{issuer => clientcertissuer}/issuer.go | 2 +- .../issuer_test.go | 2 +- internal/concierge/apiserver/apiserver.go | 4 +- internal/concierge/server/server.go | 6 +- .../federation_domain_watcher.go | 20 ++--- .../federation_domain_watcher_test.go | 38 ++++---- internal/oidc/auth/auth_handler.go | 11 +-- internal/oidc/callback/callback_handler.go | 4 +- .../idpdiscovery/idp_discovery_handler.go | 6 +- internal/oidc/login/post_login_handler.go | 4 +- ...omain_identity_providers_lister_finder.go} | 88 +++++++------------ .../federation_domain_issuer.go | 6 +- .../federation_domain_issuer_test.go | 2 +- internal/oidc/provider/manager/manager.go | 8 +- .../oidc/provider/manager/manager_test.go | 14 +-- .../resolvedprovider/resolved_provider.go | 27 ++++++ internal/oidc/token/token_handler.go | 21 ++--- internal/oidc/token/token_handler_test.go | 4 +- internal/registry/credentialrequest/rest.go | 6 +- .../registry/credentialrequest/rest_test.go | 4 +- .../testutil/oidctestutil/oidctestutil.go | 29 +++--- 23 files changed, 162 insertions(+), 152 deletions(-) rename internal/{issuer => clientcertissuer}/issuer.go (98%) rename internal/{issuer => clientcertissuer}/issuer_test.go (99%) rename internal/oidc/provider/{federation_domain_identity_providers_lister.go => federationdomainproviders/federation_domain_identity_providers_lister_finder.go} (70%) rename internal/oidc/provider/{ => federationdomainproviders}/federation_domain_issuer.go (93%) rename internal/oidc/provider/{ => federationdomainproviders}/federation_domain_issuer_test.go (98%) create mode 100644 internal/oidc/provider/resolvedprovider/resolved_provider.go diff --git a/internal/certauthority/dynamiccertauthority/dynamiccertauthority.go b/internal/certauthority/dynamiccertauthority/dynamiccertauthority.go index 42dbf7d5a..e75c7a257 100644 --- a/internal/certauthority/dynamiccertauthority/dynamiccertauthority.go +++ b/internal/certauthority/dynamiccertauthority/dynamiccertauthority.go @@ -11,7 +11,7 @@ import ( "k8s.io/apiserver/pkg/server/dynamiccertificates" "go.pinniped.dev/internal/certauthority" - "go.pinniped.dev/internal/issuer" + "go.pinniped.dev/internal/clientcertissuer" ) // ca is a type capable of issuing certificates. @@ -21,7 +21,7 @@ type ca struct { // New creates a ClientCertIssuer, ready to issue certs whenever // the given CertKeyContentProvider has a keypair to provide. -func New(provider dynamiccertificates.CertKeyContentProvider) issuer.ClientCertIssuer { +func New(provider dynamiccertificates.CertKeyContentProvider) clientcertissuer.ClientCertIssuer { return &ca{ provider: provider, } diff --git a/internal/certauthority/dynamiccertauthority/dynamiccertauthority_test.go b/internal/certauthority/dynamiccertauthority/dynamiccertauthority_test.go index b33cb9dd0..cd07d173a 100644 --- a/internal/certauthority/dynamiccertauthority/dynamiccertauthority_test.go +++ b/internal/certauthority/dynamiccertauthority/dynamiccertauthority_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/clientcertissuer" "go.pinniped.dev/internal/dynamiccert" - "go.pinniped.dev/internal/issuer" "go.pinniped.dev/internal/testutil" ) @@ -116,7 +116,7 @@ func TestCAIssuePEM(t *testing.T) { } } -func issuePEM(provider dynamiccert.Provider, ca issuer.ClientCertIssuer, caCrt, caKey []byte) ([]byte, []byte, error) { +func issuePEM(provider dynamiccert.Provider, ca clientcertissuer.ClientCertIssuer, caCrt, caKey []byte) ([]byte, []byte, error) { // if setting fails, look at that error if caCrt != nil || caKey != nil { if err := provider.SetCertKeyContent(caCrt, caKey); err != nil { diff --git a/internal/issuer/issuer.go b/internal/clientcertissuer/issuer.go similarity index 98% rename from internal/issuer/issuer.go rename to internal/clientcertissuer/issuer.go index 8023b7e75..1efbc3e7e 100644 --- a/internal/issuer/issuer.go +++ b/internal/clientcertissuer/issuer.go @@ -1,7 +1,7 @@ // Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package issuer +package clientcertissuer import ( "fmt" diff --git a/internal/issuer/issuer_test.go b/internal/clientcertissuer/issuer_test.go similarity index 99% rename from internal/issuer/issuer_test.go rename to internal/clientcertissuer/issuer_test.go index 114e5f152..82583f206 100644 --- a/internal/issuer/issuer_test.go +++ b/internal/clientcertissuer/issuer_test.go @@ -1,7 +1,7 @@ // Copyright 2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package issuer +package clientcertissuer import ( "errors" diff --git a/internal/concierge/apiserver/apiserver.go b/internal/concierge/apiserver/apiserver.go index 5fa743b03..e1e7d0743 100644 --- a/internal/concierge/apiserver/apiserver.go +++ b/internal/concierge/apiserver/apiserver.go @@ -15,8 +15,8 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" + "go.pinniped.dev/internal/clientcertissuer" "go.pinniped.dev/internal/controllerinit" - "go.pinniped.dev/internal/issuer" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/pversion" "go.pinniped.dev/internal/registry/credentialrequest" @@ -30,7 +30,7 @@ type Config struct { type ExtraConfig struct { Authenticator credentialrequest.TokenCredentialRequestAuthenticator - Issuer issuer.ClientCertIssuer + Issuer clientcertissuer.ClientCertIssuer BuildControllersPostStartHook controllerinit.RunnerBuilder Scheme *runtime.Scheme NegotiatedSerializer runtime.NegotiatedSerializer diff --git a/internal/concierge/server/server.go b/internal/concierge/server/server.go index ee446c42c..95fa26c52 100644 --- a/internal/concierge/server/server.go +++ b/internal/concierge/server/server.go @@ -23,6 +23,7 @@ import ( conciergeopenapi "go.pinniped.dev/generated/latest/client/concierge/openapi" "go.pinniped.dev/internal/certauthority/dynamiccertauthority" + "go.pinniped.dev/internal/clientcertissuer" "go.pinniped.dev/internal/concierge/apiserver" conciergescheme "go.pinniped.dev/internal/concierge/scheme" "go.pinniped.dev/internal/config/concierge" @@ -33,7 +34,6 @@ import ( "go.pinniped.dev/internal/downward" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/issuer" "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/pversion" @@ -159,7 +159,7 @@ func (a *App) runServer(ctx context.Context) error { return fmt.Errorf("could not prepare controllers: %w", err) } - certIssuer := issuer.ClientCertIssuers{ + certIssuer := clientcertissuer.ClientCertIssuers{ dynamiccertauthority.New(dynamicSigningCertProvider), // attempt to use the real Kube CA if possible dynamiccertauthority.New(impersonationProxySigningCertProvider), // fallback to our internal CA if we need to } @@ -194,7 +194,7 @@ func (a *App) runServer(ctx context.Context) error { func getAggregatedAPIServerConfig( dynamicCertProvider dynamiccert.Private, authenticator credentialrequest.TokenCredentialRequestAuthenticator, - issuer issuer.ClientCertIssuer, + issuer clientcertissuer.ClientCertIssuer, buildControllers controllerinit.RunnerBuilder, apiGroupSuffix string, aggregatedAPIServerPort int64, diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 0663469fe..0d68339e9 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -26,7 +26,7 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/plog" ) @@ -34,7 +34,7 @@ import ( // If there are no longer any valid issuers, then it can be called with no arguments. // Implementations of this type should be thread-safe to support calls from multiple goroutines. type FederationDomainsSetter interface { - SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) + SetFederationDomains(federationDomains ...*federationdomainproviders.FederationDomainIssuer) } type federationDomainWatcherController struct { @@ -145,7 +145,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro var errs []error - federationDomainIssuers := make([]*provider.FederationDomainIssuer, 0) + federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) for _, federationDomain := range federationDomains { issuerURL, urlParseErr := url.Parse(federationDomain.Spec.Issuer) @@ -187,8 +187,8 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // Create the list of IDPs for this FederationDomain. // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded // into the provider cache, so they cannot actually be used to authenticate. - federationDomainIdentityProviders := []*provider.FederationDomainIdentityProvider{} - var defaultFederationDomainIdentityProvider *provider.FederationDomainIdentityProvider + federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} + var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider if len(federationDomain.Spec.IdentityProviders) == 0 { // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) @@ -200,7 +200,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) if idpCRsCount == 1 { // If so, default that IDP's DisplayName to be the same as its resource Name. - defaultFederationDomainIdentityProvider = &provider.FederationDomainIdentityProvider{} + defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} switch { case len(oidcIdentityProviders) == 1: defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name @@ -388,7 +388,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } } // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. - federationDomainIdentityProviders = append(federationDomainIdentityProviders, &provider.FederationDomainIdentityProvider{ + federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ DisplayName: idp.DisplayName, UID: idpResourceUID, Transforms: pipeline, @@ -401,14 +401,14 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } // Now that we have the list of IDPs for this FederationDomain, create the issuer. - var federationDomainIssuer *provider.FederationDomainIssuer + var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer err = nil if defaultFederationDomainIdentityProvider != nil { // This is the constructor for the backwards compatibility mode. - federationDomainIssuer, err = provider.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) + federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) } else { // This is the constructor for any other case, including when there is an empty list of IDPs. - federationDomainIssuer, err = provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) + federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) } if err != nil { // Note that the FederationDomainIssuer constructors validate the Issuer URL. diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 66eb6e008..67b893942 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -28,7 +28,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/testutil" ) @@ -91,10 +91,10 @@ func TestInformerFilters(t *testing.T) { type fakeFederationDomainsSetter struct { SetFederationDomainsWasCalled bool - FederationDomainsReceived []*provider.FederationDomainIssuer + FederationDomainsReceived []*federationdomainproviders.FederationDomainIssuer } -func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) { +func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ...*federationdomainproviders.FederationDomainIssuer) { f.SetFederationDomainsWasCalled = true f.FederationDomainsReceived = federationDomains } @@ -196,15 +196,15 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.ElementsMatch( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ fd1, fd2, }, @@ -297,15 +297,15 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.ElementsMatch( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ fd1, fd2, }, @@ -335,10 +335,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - fd1, err := provider.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) @@ -554,12 +554,12 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - validFederationDomain, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + validFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ validFederationDomain, }, federationDomainsSetter.FederationDomainsReceived, @@ -628,12 +628,12 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - validFederationDomain, err := provider.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + validFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ validFederationDomain, }, federationDomainsSetter.FederationDomainsReceived, @@ -718,12 +718,12 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateFederationDomain, err := provider.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + nonDuplicateFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ nonDuplicateFederationDomain, }, federationDomainsSetter.FederationDomainsReceived, @@ -891,12 +891,12 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - nonDuplicateFederationDomain, err := provider.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*provider.FederationDomainIdentityProvider{}) + nonDuplicateFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) r.NoError(err) r.True(federationDomainsSetter.SetFederationDomainsWasCalled) r.Equal( - []*provider.FederationDomainIssuer{ + []*federationdomainproviders.FederationDomainIssuer{ nonDuplicateFederationDomain, }, federationDomainsSetter.FederationDomainsReceived, diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 6bb7ea092..3ea3e7ca2 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -23,8 +23,9 @@ import ( "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/login" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/oidc/provider/formposthtml" + "go.pinniped.dev/internal/oidc/provider/resolvedprovider" "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" @@ -39,7 +40,7 @@ const ( func NewHandler( downstreamIssuer string, - idpFinder provider.FederationDomainIdentityProvidersFinderI, + idpFinder federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelperWithoutStorage fosite.OAuth2Provider, oauthHelperWithStorage fosite.OAuth2Provider, generateCSRF func() (csrftoken.CSRFToken, error), @@ -213,7 +214,7 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), generatePKCE func() (pkce.Code, error), - ldapUpstream *provider.FederationDomainResolvedLDAPIdentityProvider, + ldapUpstream *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, idpType psession.ProviderType, downstreamIssuer string, upstreamStateEncoder oidc.Encoder, @@ -332,7 +333,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), generatePKCE func() (pkce.Code, error), - oidcUpstream *provider.FederationDomainResolvedOIDCIdentityProvider, + oidcUpstream *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, downstreamIssuer string, upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, @@ -448,7 +449,7 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { // chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error. // Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. -func chooseUpstreamIDP(idpDisplayName string, idpLister provider.FederationDomainIdentityProvidersFinderI) (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { +func chooseUpstreamIDP(idpDisplayName string, idpLister federationdomainproviders.FederationDomainIdentityProvidersFinderI) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { // When a request is made to the authorization endpoint which does not specify the IDP name, then it might // be an old dynamic client (OIDCClient). We need to make this work, but only in the backwards compatibility case // where there is exactly one IDP defined in the namespace and no IDPs listed on the FederationDomain. diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index e260d201c..5613108ce 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -14,13 +14,13 @@ import ( "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" ) func NewHandler( - upstreamIDPs provider.FederationDomainIdentityProvidersFinderI, + upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider, stateDecoder, cookieDecoder oidc.Decoder, redirectURI string, diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/oidc/idpdiscovery/idp_discovery_handler.go index 0b93b2b72..d9034828f 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler.go @@ -11,11 +11,11 @@ import ( "sort" "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" ) // NewHandler returns an http.Handler that serves the upstream IDP discovery endpoint. -func NewHandler(upstreamIDPs provider.FederationDomainIdentityProvidersListerI) http.Handler { +func NewHandler(upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersListerI) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) @@ -36,7 +36,7 @@ func NewHandler(upstreamIDPs provider.FederationDomainIdentityProvidersListerI) }) } -func responseAsJSON(upstreamIDPs provider.FederationDomainIdentityProvidersListerI) ([]byte, error) { +func responseAsJSON(upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersListerI) ([]byte, error) { r := v1alpha1.IDPDiscoveryResponse{PinnipedIDPs: []v1alpha1.PinnipedIDP{}} // The cache of IDPs could change at any time, so always recalculate the list. diff --git a/internal/oidc/login/post_login_handler.go b/internal/oidc/login/post_login_handler.go index 8c9a59702..a9b0c7fcd 100644 --- a/internal/oidc/login/post_login_handler.go +++ b/internal/oidc/login/post_login_handler.go @@ -12,11 +12,11 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/plog" ) -func NewPostHandler(issuerURL string, upstreamIDPs provider.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider) HandlerFunc { +func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { // Note that the login handler prevents this handler from being called with OIDC upstreams. _, ldapUpstream, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName) diff --git a/internal/oidc/provider/federation_domain_identity_providers_lister.go b/internal/oidc/provider/federationdomainproviders/federation_domain_identity_providers_lister_finder.go similarity index 70% rename from internal/oidc/provider/federation_domain_identity_providers_lister.go rename to internal/oidc/provider/federationdomainproviders/federation_domain_identity_providers_lister_finder.go index 42d47ed3c..27d76a1dd 100644 --- a/internal/oidc/provider/federation_domain_identity_providers_lister.go +++ b/internal/oidc/provider/federationdomainproviders/federation_domain_identity_providers_lister_finder.go @@ -1,7 +1,7 @@ // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package provider +package federationdomainproviders import ( "fmt" @@ -11,7 +11,7 @@ import ( "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc/idplister" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/oidc/provider/resolvedprovider" "go.pinniped.dev/internal/psession" ) @@ -24,44 +24,24 @@ type FederationDomainIdentityProvider struct { Transforms *idtransform.TransformationPipeline } -// FederationDomainResolvedOIDCIdentityProvider represents a FederationDomainIdentityProvider which has -// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamOIDCIdentityProviderI -// and other metadata about the provider. -type FederationDomainResolvedOIDCIdentityProvider struct { - DisplayName string - Provider upstreamprovider.UpstreamOIDCIdentityProviderI - SessionProviderType psession.ProviderType - Transforms *idtransform.TransformationPipeline -} - -// FederationDomainResolvedLDAPIdentityProvider represents a FederationDomainIdentityProvider which has -// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamLDAPIdentityProviderI -// and other metadata about the provider. -type FederationDomainResolvedLDAPIdentityProvider struct { - DisplayName string - Provider upstreamprovider.UpstreamLDAPIdentityProviderI - SessionProviderType psession.ProviderType - Transforms *idtransform.TransformationPipeline -} - type FederationDomainIdentityProvidersFinderI interface { FindDefaultIDP() ( - *FederationDomainResolvedOIDCIdentityProvider, - *FederationDomainResolvedLDAPIdentityProvider, + *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, + *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error, ) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( - *FederationDomainResolvedOIDCIdentityProvider, - *FederationDomainResolvedLDAPIdentityProvider, + *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, + *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error, ) } type FederationDomainIdentityProvidersListerI interface { - GetOIDCIdentityProviders() []*FederationDomainResolvedOIDCIdentityProvider - GetLDAPIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider - GetActiveDirectoryIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider + GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider + GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider } type FederationDomainIdentityProvidersListerFinderI interface { @@ -69,16 +49,16 @@ type FederationDomainIdentityProvidersListerFinderI interface { FederationDomainIdentityProvidersFinderI } -// FederationDomainIdentityProvidersLister implements FederationDomainIdentityProvidersListerFinderI. -var _ FederationDomainIdentityProvidersListerFinderI = (*FederationDomainIdentityProvidersLister)(nil) +// FederationDomainIdentityProvidersListerFinder implements FederationDomainIdentityProvidersListerFinderI. +var _ FederationDomainIdentityProvidersListerFinderI = (*FederationDomainIdentityProvidersListerFinder)(nil) -// FederationDomainIdentityProvidersLister wraps an UpstreamIdentityProvidersLister. The lister which is being +// FederationDomainIdentityProvidersListerFinder wraps an UpstreamIdentityProvidersLister. The lister which is being // wrapped should contain all valid upstream providers that are currently defined in the Supervisor. -// FederationDomainIdentityProvidersLister provides a lookup method which only looks up IDPs within those which +// FederationDomainIdentityProvidersListerFinder provides a lookup method which only looks up IDPs within those which // have allowed resource IDs, and also uses display names (name aliases) instead of the actual resource names to do the // lookups. It also provides list methods which only list the allowed identity providers (to be used by the IDP // discovery endpoint, for example). -type FederationDomainIdentityProvidersLister struct { +type FederationDomainIdentityProvidersListerFinder struct { wrappedLister idplister.UpstreamIdentityProvidersLister configuredIdentityProviders []*FederationDomainIdentityProvider defaultIdentityProvider *FederationDomainIdentityProvider @@ -86,17 +66,17 @@ type FederationDomainIdentityProvidersLister struct { allowedIDPResourceUIDs sets.Set[types.UID] } -// NewFederationDomainUpstreamIdentityProvidersLister returns a new FederationDomainIdentityProvidersLister +// NewFederationDomainIdentityProvidersListerFinder returns a new FederationDomainIdentityProvidersListerFinder // which only lists those IDPs allowed by its parameter. Every FederationDomainIdentityProvider in the // federationDomainIssuer parameter's IdentityProviders() list must have a unique DisplayName. // Note that a single underlying IDP UID may be used by multiple FederationDomainIdentityProvider in the parameter. // The wrapped lister should contain all valid upstream providers that are defined in the Supervisor, and is expected to -// be thread-safe and to change its contents over time. The FederationDomainIdentityProvidersLister will filter out the +// be thread-safe and to change its contents over time. The FederationDomainIdentityProvidersListerFinder will filter out the // ones that don't apply to this federation domain. -func NewFederationDomainUpstreamIdentityProvidersLister( +func NewFederationDomainIdentityProvidersListerFinder( federationDomainIssuer *FederationDomainIssuer, wrappedLister idplister.UpstreamIdentityProvidersLister, -) *FederationDomainIdentityProvidersLister { +) *FederationDomainIdentityProvidersListerFinder { // Create a copy of the input slice so we won't need to worry about the caller accidentally changing it. copyOfFederationDomainIdentityProviders := []*FederationDomainIdentityProvider{} // Create a map and a set for quick lookups of the same data that was passed in via the @@ -110,7 +90,7 @@ func NewFederationDomainUpstreamIdentityProvidersLister( copyOfFederationDomainIdentityProviders = append(copyOfFederationDomainIdentityProviders, &shallowCopyOfIDP) } - return &FederationDomainIdentityProvidersLister{ + return &FederationDomainIdentityProvidersListerFinder{ wrappedLister: wrappedLister, configuredIdentityProviders: copyOfFederationDomainIdentityProviders, defaultIdentityProvider: federationDomainIssuer.DefaultIdentityProvider(), @@ -122,9 +102,9 @@ func NewFederationDomainUpstreamIdentityProvidersLister( // FindUpstreamIDPByDisplayName selects either an OIDC, LDAP, or ActiveDirectory IDP, or returns an error. // It only considers the allowed IDPs while doing the lookup by display name. // Note that ActiveDirectory and LDAP IDPs both return the same type, but with different SessionProviderType values. -func (u *FederationDomainIdentityProvidersLister) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( - *FederationDomainResolvedOIDCIdentityProvider, - *FederationDomainResolvedLDAPIdentityProvider, +func (u *FederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( + *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, + *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error, ) { // Given a display name, look up the identity provider's UID for that display name. @@ -156,9 +136,9 @@ func (u *FederationDomainIdentityProvidersLister) FindUpstreamIDPByDisplayName(u // This can be used to handle the backwards compatibility mode where an authorization request could be made // without specifying an IDP name, and there are no IDPs explicitly specified on the FederationDomain, and there // is exactly one IDP CR defined in the Supervisor namespace. -func (u *FederationDomainIdentityProvidersLister) FindDefaultIDP() ( - *FederationDomainResolvedOIDCIdentityProvider, - *FederationDomainResolvedLDAPIdentityProvider, +func (u *FederationDomainIdentityProvidersListerFinder) FindDefaultIDP() ( + *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, + *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error, ) { if u.defaultIdentityProvider == nil { @@ -168,10 +148,10 @@ func (u *FederationDomainIdentityProvidersLister) FindDefaultIDP() ( } // GetOIDCIdentityProviders lists only the OIDC providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersLister) GetOIDCIdentityProviders() []*FederationDomainResolvedOIDCIdentityProvider { +func (u *FederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider { // Get the cached providers once at the start in case they change during the rest of this function. cachedProviders := u.wrappedLister.GetOIDCIdentityProviders() - providers := []*FederationDomainResolvedOIDCIdentityProvider{} + providers := []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{} // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... for _, idp := range u.configuredIdentityProviders { @@ -179,7 +159,7 @@ func (u *FederationDomainIdentityProvidersLister) GetOIDCIdentityProviders() []* for _, p := range cachedProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &FederationDomainResolvedOIDCIdentityProvider{ + providers = append(providers, &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeOIDC, @@ -192,10 +172,10 @@ func (u *FederationDomainIdentityProvidersLister) GetOIDCIdentityProviders() []* } // GetLDAPIdentityProviders lists only the LDAP providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersLister) GetLDAPIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider { +func (u *FederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { // Get the cached providers once at the start in case they change during the rest of this function. cachedProviders := u.wrappedLister.GetLDAPIdentityProviders() - providers := []*FederationDomainResolvedLDAPIdentityProvider{} + providers := []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{} // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... for _, idp := range u.configuredIdentityProviders { @@ -203,7 +183,7 @@ func (u *FederationDomainIdentityProvidersLister) GetLDAPIdentityProviders() []* for _, p := range cachedProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &FederationDomainResolvedLDAPIdentityProvider{ + providers = append(providers, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeLDAP, @@ -216,10 +196,10 @@ func (u *FederationDomainIdentityProvidersLister) GetLDAPIdentityProviders() []* } // GetActiveDirectoryIdentityProviders lists only the ActiveDirectory providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersLister) GetActiveDirectoryIdentityProviders() []*FederationDomainResolvedLDAPIdentityProvider { +func (u *FederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { // Get the cached providers once at the start in case they change during the rest of this function. cachedProviders := u.wrappedLister.GetActiveDirectoryIdentityProviders() - providers := []*FederationDomainResolvedLDAPIdentityProvider{} + providers := []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{} // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... for _, idp := range u.configuredIdentityProviders { @@ -227,7 +207,7 @@ func (u *FederationDomainIdentityProvidersLister) GetActiveDirectoryIdentityProv for _, p := range cachedProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &FederationDomainResolvedLDAPIdentityProvider{ + providers = append(providers, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeActiveDirectory, diff --git a/internal/oidc/provider/federation_domain_issuer.go b/internal/oidc/provider/federationdomainproviders/federation_domain_issuer.go similarity index 93% rename from internal/oidc/provider/federation_domain_issuer.go rename to internal/oidc/provider/federationdomainproviders/federation_domain_issuer.go index bdb27f142..7d937c491 100644 --- a/internal/oidc/provider/federation_domain_issuer.go +++ b/internal/oidc/provider/federationdomainproviders/federation_domain_issuer.go @@ -1,7 +1,7 @@ // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package provider +package federationdomainproviders import ( "fmt" @@ -11,8 +11,8 @@ import ( "go.pinniped.dev/internal/constable" ) -// FederationDomainIssuer represents all the settings and state for a downstream OIDC provider -// as defined by a FederationDomain. +// FederationDomainIssuer is a parsed FederationDomain representing all the settings for a downstream OIDC provider +// and contains configuration representing a set of upstream identity providers. type FederationDomainIssuer struct { issuer string issuerHost string diff --git a/internal/oidc/provider/federation_domain_issuer_test.go b/internal/oidc/provider/federationdomainproviders/federation_domain_issuer_test.go similarity index 98% rename from internal/oidc/provider/federation_domain_issuer_test.go rename to internal/oidc/provider/federationdomainproviders/federation_domain_issuer_test.go index 08a3d8514..cd4c97fba 100644 --- a/internal/oidc/provider/federation_domain_issuer_test.go +++ b/internal/oidc/provider/federationdomainproviders/federation_domain_issuer_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package provider +package federationdomainproviders import ( "testing" diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 2c8df7a0a..2d0e760a3 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -23,7 +23,7 @@ import ( "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/login" "go.pinniped.dev/internal/oidc/oidcclientvalidator" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/oidc/token" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/secret" @@ -36,7 +36,7 @@ import ( // It is thread-safe. type Manager struct { mu sync.RWMutex - providers []*provider.FederationDomainIssuer + providers []*federationdomainproviders.FederationDomainIssuer providerHandlers map[string]http.Handler // map of all routes for all providers nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data @@ -77,7 +77,7 @@ func NewManager( // // This method assumes that all of the FederationDomainIssuer arguments have already been validated // by someone else before they are passed to this method. -func (m *Manager) SetFederationDomains(federationDomains ...*provider.FederationDomainIssuer) { +func (m *Manager) SetFederationDomains(federationDomains ...*federationdomainproviders.FederationDomainIssuer) { m.mu.Lock() defer m.mu.Unlock() @@ -123,7 +123,7 @@ func (m *Manager) SetFederationDomains(federationDomains ...*provider.Federation wrapGetter(incomingFederationDomain.Issuer(), m.secretCache.GetStateEncoderBlockKey), ) - idpLister := provider.NewFederationDomainUpstreamIdentityProvidersLister(incomingFederationDomain, m.upstreamIDPs) + idpLister := federationdomainproviders.NewFederationDomainIdentityProvidersListerFinder(incomingFederationDomain, m.upstreamIDPs) m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuerURL) diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 9f02d9cb4..8cf764c68 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -25,7 +25,7 @@ import ( "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/secret" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -39,7 +39,7 @@ func TestManager(t *testing.T) { nextHandler http.HandlerFunc fallbackHandlerWasCalled bool dynamicJWKSProvider jwks.DynamicJWKSProvider - federationDomainIDPs []*provider.FederationDomainIdentityProvider + federationDomainIDPs []*federationdomainproviders.FederationDomainIdentityProvider kubeClient *fake.Clientset ) @@ -249,7 +249,7 @@ func TestManager(t *testing.T) { parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) r.NoError(err) - federationDomainIDPs = []*provider.FederationDomainIdentityProvider{ + federationDomainIDPs = []*federationdomainproviders.FederationDomainIdentityProvider{ { DisplayName: upstreamIDPName, UID: upstreamResourceUID, @@ -391,9 +391,9 @@ func TestManager(t *testing.T) { when("given some valid providers via SetFederationDomains()", func() { it.Before(func() { - fd1, err := provider.NewFederationDomainIssuer(issuer1, federationDomainIDPs) + fd1, err := federationdomainproviders.NewFederationDomainIssuer(issuer1, federationDomainIDPs) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(issuer2, federationDomainIDPs) + fd2, err := federationdomainproviders.NewFederationDomainIssuer(issuer2, federationDomainIDPs) r.NoError(err) subject.SetFederationDomains(fd1, fd2) @@ -434,9 +434,9 @@ func TestManager(t *testing.T) { when("given the same valid providers as arguments to SetFederationDomains() in reverse order", func() { it.Before(func() { - fd1, err := provider.NewFederationDomainIssuer(issuer1, federationDomainIDPs) + fd1, err := federationdomainproviders.NewFederationDomainIssuer(issuer1, federationDomainIDPs) r.NoError(err) - fd2, err := provider.NewFederationDomainIssuer(issuer2, federationDomainIDPs) + fd2, err := federationdomainproviders.NewFederationDomainIssuer(issuer2, federationDomainIDPs) r.NoError(err) subject.SetFederationDomains(fd2, fd1) diff --git a/internal/oidc/provider/resolvedprovider/resolved_provider.go b/internal/oidc/provider/resolvedprovider/resolved_provider.go new file mode 100644 index 000000000..f37f0a161 --- /dev/null +++ b/internal/oidc/provider/resolvedprovider/resolved_provider.go @@ -0,0 +1,27 @@ +package resolvedprovider + +import ( + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/psession" +) + +// FederationDomainResolvedOIDCIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamOIDCIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedOIDCIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamOIDCIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +// FederationDomainResolvedLDAPIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamLDAPIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedLDAPIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamLDAPIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index 09cb1596d..cddca4551 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -22,14 +22,15 @@ import ( "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" + "go.pinniped.dev/internal/oidc/provider/resolvedprovider" "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) func NewHandler( - idpLister provider.FederationDomainIdentityProvidersListerI, + idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, oauthHelper fosite.OAuth2Provider, ) http.Handler { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { @@ -97,7 +98,7 @@ func errUpstreamRefreshError() *fosite.RFC6749Error { } } -func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, idpLister provider.FederationDomainIdentityProvidersListerI) error { +func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI) error { session := accessRequest.GetSession().(*psession.PinnipedSession) customSessionData := session.Custom @@ -129,7 +130,7 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, func upstreamOIDCRefresh( ctx context.Context, session *psession.PinnipedSession, - idpLister provider.FederationDomainIdentityProvidersListerI, + idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, grantedScopes []string, clientID string, ) error { @@ -310,8 +311,8 @@ func getString(m map[string]interface{}, key string) (string, bool) { func findOIDCProviderByNameAndValidateUID( s *psession.CustomSessionData, - idpLister provider.FederationDomainIdentityProvidersListerI, -) (*provider.FederationDomainResolvedOIDCIdentityProvider, error) { + idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, +) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, error) { for _, p := range idpLister.GetOIDCIdentityProviders() { if p.Provider.GetName() == s.ProviderName { if p.Provider.GetResourceUID() != s.ProviderUID { @@ -328,7 +329,7 @@ func findOIDCProviderByNameAndValidateUID( func upstreamLDAPRefresh( ctx context.Context, - idpLister provider.FederationDomainIdentityProvidersListerI, + idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, session *psession.PinnipedSession, grantedScopes []string, clientID string, @@ -439,9 +440,9 @@ func transformRefreshedIdentity( func findLDAPProviderByNameAndValidateUID( s *psession.CustomSessionData, - idpLister provider.FederationDomainIdentityProvidersListerI, -) (*provider.FederationDomainResolvedLDAPIdentityProvider, string, error) { - var providers []*provider.FederationDomainResolvedLDAPIdentityProvider + idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, +) (*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, string, error) { + var providers []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider var dn string if s.ProviderType == psession.ProviderTypeLDAP { providers = idpLister.GetLDAPIdentityProviders() diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 494346d2f..aaeef0f6c 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -54,7 +54,7 @@ import ( "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/oidcclientvalidator" - "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/oidcclientsecretstorage" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" @@ -4170,7 +4170,7 @@ func requireClaimsAreEqual(t *testing.T, claimName string, claimsOfTokenA map[st func exchangeAuthcodeForTokens( t *testing.T, test authcodeExchangeInputs, - idps provider.FederationDomainIdentityProvidersListerFinderI, + idps federationdomainproviders.FederationDomainIdentityProvidersListerFinderI, kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset), ) ( subject http.Handler, diff --git a/internal/registry/credentialrequest/rest.go b/internal/registry/credentialrequest/rest.go index 4e2722662..4a5631d04 100644 --- a/internal/registry/credentialrequest/rest.go +++ b/internal/registry/credentialrequest/rest.go @@ -21,7 +21,7 @@ import ( "k8s.io/utils/trace" loginapi "go.pinniped.dev/generated/latest/apis/concierge/login" - "go.pinniped.dev/internal/issuer" + "go.pinniped.dev/internal/clientcertissuer" ) // clientCertificateTTL is the TTL for short-lived client certificates returned by this API. @@ -31,7 +31,7 @@ type TokenCredentialRequestAuthenticator interface { AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error) } -func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer issuer.ClientCertIssuer, resource schema.GroupResource) *REST { +func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer clientcertissuer.ClientCertIssuer, resource schema.GroupResource) *REST { return &REST{ authenticator: authenticator, issuer: issuer, @@ -41,7 +41,7 @@ func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer issuer.Cl type REST struct { authenticator TokenCredentialRequestAuthenticator - issuer issuer.ClientCertIssuer + issuer clientcertissuer.ClientCertIssuer tableConvertor rest.TableConvertor } diff --git a/internal/registry/credentialrequest/rest_test.go b/internal/registry/credentialrequest/rest_test.go index 3ad728444..a9bff18fa 100644 --- a/internal/registry/credentialrequest/rest_test.go +++ b/internal/registry/credentialrequest/rest_test.go @@ -25,7 +25,7 @@ import ( "k8s.io/utils/ptr" loginapi "go.pinniped.dev/generated/latest/apis/concierge/login" - "go.pinniped.dev/internal/issuer" + "go.pinniped.dev/internal/clientcertissuer" "go.pinniped.dev/internal/mocks/credentialrequestmocks" "go.pinniped.dev/internal/mocks/issuermocks" "go.pinniped.dev/internal/testutil" @@ -392,7 +392,7 @@ func requireSuccessfulResponseWithAuthenticationFailureMessage(t *testing.T, err }) } -func successfulIssuer(ctrl *gomock.Controller) issuer.ClientCertIssuer { +func successfulIssuer(ctrl *gomock.Controller) clientcertissuer.ClientCertIssuer { clientCertIssuer := issuermocks.NewMockClientCertIssuer(ctrl) clientCertIssuer.EXPECT(). IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()). diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index a574c7811..e879ffcf3 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -36,6 +36,7 @@ import ( "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/resolvedprovider" "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" @@ -470,10 +471,10 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) SetDefaultIDPDisplay t.defaultIDPDisplayName = displayName } -func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*provider.FederationDomainResolvedOIDCIdentityProvider { - fdIDPs := make([]*provider.FederationDomainResolvedOIDCIdentityProvider, len(t.upstreamOIDCIdentityProviders)) +func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider { + fdIDPs := make([]*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, len(t.upstreamOIDCIdentityProviders)) for i, testIDP := range t.upstreamOIDCIdentityProviders { - fdIDP := &provider.FederationDomainResolvedOIDCIdentityProvider{ + fdIDP := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeOIDC, @@ -484,10 +485,10 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProvi return fdIDPs } -func (t *TestFederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*provider.FederationDomainResolvedLDAPIdentityProvider { - fdIDPs := make([]*provider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamLDAPIdentityProviders)) +func (t *TestFederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { + fdIDPs := make([]*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamLDAPIdentityProviders)) for i, testIDP := range t.upstreamLDAPIdentityProviders { - fdIDP := &provider.FederationDomainResolvedLDAPIdentityProvider{ + fdIDP := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeLDAP, @@ -498,10 +499,10 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProvi return fdIDPs } -func (t *TestFederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*provider.FederationDomainResolvedLDAPIdentityProvider { - fdIDPs := make([]*provider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamActiveDirectoryIdentityProviders)) +func (t *TestFederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { + fdIDPs := make([]*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamActiveDirectoryIdentityProviders)) for i, testIDP := range t.upstreamActiveDirectoryIdentityProviders { - fdIDP := &provider.FederationDomainResolvedLDAPIdentityProvider{ + fdIDP := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeActiveDirectory, @@ -512,17 +513,17 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) GetActiveDirectoryId return fdIDPs } -func (t *TestFederationDomainIdentityProvidersListerFinder) FindDefaultIDP() (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { +func (t *TestFederationDomainIdentityProvidersListerFinder) FindDefaultIDP() (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { if t.defaultIDPDisplayName == "" { return nil, nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") } return t.FindUpstreamIDPByDisplayName(t.defaultIDPDisplayName) } -func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (*provider.FederationDomainResolvedOIDCIdentityProvider, *provider.FederationDomainResolvedLDAPIdentityProvider, error) { +func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { for _, testIDP := range t.upstreamOIDCIdentityProviders { if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return &provider.FederationDomainResolvedOIDCIdentityProvider{ + return &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeOIDC, @@ -532,7 +533,7 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDis } for _, testIDP := range t.upstreamLDAPIdentityProviders { if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return nil, &provider.FederationDomainResolvedLDAPIdentityProvider{ + return nil, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeLDAP, @@ -542,7 +543,7 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDis } for _, testIDP := range t.upstreamActiveDirectoryIdentityProviders { if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return nil, &provider.FederationDomainResolvedLDAPIdentityProvider{ + return nil, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: testIDP.DisplayNameForFederationDomain, Provider: testIDP, SessionProviderType: psession.ProviderTypeActiveDirectory, From 86c791b8a68016d7eebd9fb2126d5bfc6219ee42 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 22 Jun 2023 15:12:33 -0700 Subject: [PATCH 15/81] reorganize federation domain packages to be more intuitive Co-authored-by: Benjamin A. Petersen --- .../active_directory_upstream_watcher.go | 2 +- .../active_directory_upstream_watcher_test.go | 6 +- .../federation_domain_watcher.go | 2 +- .../federation_domain_watcher_test.go | 2 +- .../ldap_upstream_watcher.go | 2 +- .../ldap_upstream_watcher_test.go | 6 +- .../oidcclientwatcher/oidc_client_watcher.go | 2 +- .../oidc_upstream_watcher.go | 4 +- .../oidc_upstream_watcher_test.go | 8 +- .../upstreamwatchers/upstream_watchers.go | 2 +- .../supervisorstorage/garbage_collector.go | 6 +- .../garbage_collector_test.go | 10 +- .../clientregistry/clientregistry.go | 4 +- .../clientregistry/clientregistry_test.go | 4 +- .../provider => federationdomain}/csp/csp.go | 2 +- .../csp/csp_test.go | 2 +- .../csrftoken/csrftoken.go | 2 +- .../csrftoken/csrftoken_test.go | 2 +- .../downstreamsession/downstream_session.go | 4 +- .../downstream_session_test.go | 0 .../dynamiccodec/codec.go | 4 +- .../dynamiccodec/codec_test.go | 2 +- .../dynamic_tls_cert_provider.go | 4 +- .../dynamic_upstream_idp_provider.go | 4 +- .../endpoints}/auth/auth_handler.go | 16 ++-- .../endpoints}/auth/auth_handler_test.go | 19 ++-- .../endpoints}/callback/callback_handler.go | 8 +- .../callback/callback_handler_test.go | 9 +- .../endpoints}/discovery/discovery_handler.go | 2 +- .../discovery/discovery_handler_test.go | 2 +- .../idpdiscovery/idp_discovery_handler.go | 2 +- .../idp_discovery_handler_test.go | 2 +- .../endpoints}/jwks/dynamic_jwks_provider.go | 2 +- .../endpoints}/jwks/jwks_handler.go | 2 +- .../endpoints}/jwks/jwks_handler_test.go | 2 +- .../endpoints}/login/get_login_handler.go | 6 +- .../login/get_login_handler_test.go | 6 +- .../endpoints}/login/login_handler.go | 8 +- .../endpoints}/login/login_handler_test.go | 4 +- .../endpoints}/login/loginhtml/login_form.css | 2 +- .../login/loginhtml/login_form.gohtml | 2 +- .../endpoints}/login/loginhtml/loginhtml.go | 4 +- .../login/loginhtml/loginhtml_test.go | 6 +- .../endpoints}/login/post_login_handler.go | 6 +- .../login/post_login_handler_test.go | 9 +- .../endpoints}/token/token_handler.go | 10 +- .../endpoints}/token/token_handler_test.go | 47 +++++----- .../tokenexchange}/token_exchange.go | 28 +++--- .../endpointsmanager}/manager.go | 35 +++---- .../endpointsmanager}/manager_test.go | 10 +- ...domain_identity_providers_lister_finder.go | 4 +- .../federation_domain_issuer.go | 0 .../federation_domain_issuer_test.go | 0 .../formposthtml/form_post.css | 2 +- .../formposthtml/form_post.gohtml | 2 +- .../formposthtml/form_post.js | 2 +- .../formposthtml/formposthtml.go | 4 +- .../formposthtml/formposthtml_test.go | 2 +- .../idplister/upstream_idp_lister.go | 2 +- internal/{ => federationdomain}/oidc/oidc.go | 92 +++---------------- .../oidcclientvalidator.go | 0 .../resolvedprovider/resolved_provider.go | 5 +- .../storage}/dynamic_global_secret_config.go | 4 +- .../storage}/kube_storage.go | 9 +- .../storage/null_storage.go} | 6 +- .../strategy}/dynamic_oauth2_hmac_strategy.go | 40 ++++---- .../dynamic_oauth2_hmac_strategy_test.go | 12 +-- .../dynamic_open_id_connect_ecdsa_strategy.go | 20 ++-- ...mic_open_id_connect_ecdsa_strategy_test.go | 8 +- .../timeouts/timeouts_configuration.go | 74 +++++++++++++++ .../upstreamprovider/upsteam_provider.go | 0 .../fositestorage/accesstoken/accesstoken.go | 2 +- .../accesstoken/accesstoken_test.go | 2 +- .../authorizationcode/authorizationcode.go | 2 +- .../authorizationcode_test.go | 2 +- internal/fositestorage/fositestorage.go | 4 +- .../openidconnect/openidconnect.go | 2 +- .../openidconnect/openidconnect_test.go | 2 +- internal/fositestorage/pkce/pkce.go | 2 +- internal/fositestorage/pkce/pkce_test.go | 2 +- .../refreshtoken/refreshtoken.go | 2 +- .../refreshtoken/refreshtoken_test.go | 2 +- internal/mocks/issuermocks/generate.go | 4 +- internal/mocks/issuermocks/issuermocks.go | 2 +- .../generate.go | 2 +- .../mockupstreamoidcidentityprovider.go | 4 +- internal/supervisor/server/server.go | 19 ++-- internal/testutil/oidcclient_test.go | 6 +- .../testutil/oidctestutil/oidctestutil.go | 24 ++--- internal/upstreamldap/upstreamldap.go | 4 +- internal/upstreamldap/upstreamldap_test.go | 2 +- internal/upstreamoidc/upstreamoidc.go | 8 +- internal/upstreamoidc/upstreamoidc_test.go | 8 +- pkg/oidcclient/login.go | 2 +- pkg/oidcclient/login_test.go | 2 +- proposals/1113_ldap-ad-web-ui/README.md | 2 +- .../docs/reference/code-walkthrough.md | 18 ++-- test/integration/formposthtml_test.go | 2 +- test/integration/supervisor_login_test.go | 9 +- test/integration/supervisor_storage_test.go | 4 +- test/integration/supervisor_warnings_test.go | 9 +- 101 files changed, 401 insertions(+), 377 deletions(-) rename internal/{oidc => federationdomain}/clientregistry/clientregistry.go (98%) rename internal/{oidc => federationdomain}/clientregistry/clientregistry_test.go (99%) rename internal/{oidc/provider => federationdomain}/csp/csp.go (81%) rename internal/{oidc/provider => federationdomain}/csp/csp_test.go (81%) rename internal/{oidc => federationdomain}/csrftoken/csrftoken.go (87%) rename internal/{oidc => federationdomain}/csrftoken/csrftoken_test.go (84%) rename internal/{oidc => federationdomain}/downstreamsession/downstream_session.go (99%) rename internal/{oidc => federationdomain}/downstreamsession/downstream_session_test.go (100%) rename internal/{oidc => federationdomain}/dynamiccodec/codec.go (93%) rename internal/{oidc => federationdomain}/dynamiccodec/codec_test.go (98%) rename internal/{oidc/provider => federationdomain/dynamictlscertprovider}/dynamic_tls_cert_provider.go (93%) rename internal/{oidc/provider => federationdomain/dynamicupstreamprovider}/dynamic_upstream_idp_provider.go (97%) rename internal/{oidc => federationdomain/endpoints}/auth/auth_handler.go (98%) rename internal/{oidc => federationdomain/endpoints}/auth/auth_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints}/callback/callback_handler.go (95%) rename internal/{oidc => federationdomain/endpoints}/callback/callback_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints}/discovery/discovery_handler.go (98%) rename internal/{oidc => federationdomain/endpoints}/discovery/discovery_handler_test.go (98%) rename internal/{oidc => federationdomain/endpoints}/idpdiscovery/idp_discovery_handler.go (97%) rename internal/{oidc => federationdomain/endpoints}/idpdiscovery/idp_discovery_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints}/jwks/dynamic_jwks_provider.go (94%) rename internal/{oidc => federationdomain/endpoints}/jwks/jwks_handler.go (92%) rename internal/{oidc => federationdomain/endpoints}/jwks/jwks_handler_test.go (97%) rename internal/{oidc => federationdomain/endpoints}/login/get_login_handler.go (84%) rename internal/{oidc => federationdomain/endpoints}/login/get_login_handler_test.go (95%) rename internal/{oidc => federationdomain/endpoints}/login/login_handler.go (94%) rename internal/{oidc => federationdomain/endpoints}/login/login_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints}/login/loginhtml/login_form.css (96%) rename internal/{oidc => federationdomain/endpoints}/login/loginhtml/login_form.gohtml (99%) rename internal/{oidc => federationdomain/endpoints}/login/loginhtml/loginhtml.go (94%) rename internal/{oidc => federationdomain/endpoints}/login/loginhtml/loginhtml_test.go (97%) rename internal/{oidc => federationdomain/endpoints}/login/post_login_handler.go (96%) rename internal/{oidc => federationdomain/endpoints}/login/post_login_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints}/token/token_handler.go (98%) rename internal/{oidc => federationdomain/endpoints}/token/token_handler_test.go (99%) rename internal/{oidc => federationdomain/endpoints/tokenexchange}/token_exchange.go (91%) rename internal/{oidc/provider/manager => federationdomain/endpointsmanager}/manager.go (86%) rename internal/{oidc/provider/manager => federationdomain/endpointsmanager}/manager_test.go (98%) rename internal/{oidc/provider => federationdomain}/federationdomainproviders/federation_domain_identity_providers_lister_finder.go (98%) rename internal/{oidc/provider => federationdomain}/federationdomainproviders/federation_domain_issuer.go (100%) rename internal/{oidc/provider => federationdomain}/federationdomainproviders/federation_domain_issuer_test.go (100%) rename internal/{oidc/provider => federationdomain}/formposthtml/form_post.css (97%) rename internal/{oidc/provider => federationdomain}/formposthtml/form_post.gohtml (94%) rename internal/{oidc/provider => federationdomain}/formposthtml/form_post.js (98%) rename internal/{oidc/provider => federationdomain}/formposthtml/formposthtml.go (94%) rename internal/{oidc/provider => federationdomain}/formposthtml/formposthtml_test.go (99%) rename internal/{oidc => federationdomain}/idplister/upstream_idp_lister.go (92%) rename internal/{ => federationdomain}/oidc/oidc.go (70%) rename internal/{oidc => federationdomain}/oidcclientvalidator/oidcclientvalidator.go (100%) rename internal/{oidc/provider => federationdomain}/resolvedprovider/resolved_provider.go (87%) rename internal/{oidc => federationdomain/storage}/dynamic_global_secret_config.go (96%) rename internal/{oidc => federationdomain/storage}/kube_storage.go (97%) rename internal/{oidc/nullstorage.go => federationdomain/storage/null_storage.go} (96%) rename internal/{oidc => federationdomain/strategy}/dynamic_oauth2_hmac_strategy.go (81%) rename internal/{oidc => federationdomain/strategy}/dynamic_oauth2_hmac_strategy_test.go (97%) rename internal/{oidc => federationdomain/strategy}/dynamic_open_id_connect_ecdsa_strategy.go (79%) rename internal/{oidc => federationdomain/strategy}/dynamic_open_id_connect_ecdsa_strategy_test.go (95%) create mode 100644 internal/federationdomain/timeouts/timeouts_configuration.go rename internal/{oidc/provider => federationdomain}/upstreamprovider/upsteam_provider.go (100%) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index b1f214968..139592f0f 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -26,7 +26,7 @@ import ( "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) 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 c223441d7..f88287a01 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -29,9 +29,9 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/mocks/mockldapconn" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/upstreamldap" ) @@ -2010,7 +2010,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) - cache := provider.NewDynamicUpstreamIDPProvider() + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetActiveDirectoryIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 0d68339e9..3923b498c 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -25,8 +25,8 @@ import ( "go.pinniped.dev/internal/celtransformer" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/plog" ) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 67b893942..0e75cdd34 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -27,8 +27,8 @@ import ( pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index fcf3a7e35..fe129588f 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -20,7 +20,7 @@ import ( "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index 46b0b1a8a..4da286737 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -28,9 +28,9 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/mocks/mockldapconn" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/upstreamldap" ) @@ -1139,7 +1139,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) - cache := provider.NewDynamicUpstreamIDPProvider() + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetLDAPIdentityProviders([]upstreamprovider.UpstreamLDAPIdentityProviderI{ upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) diff --git a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go index fbd54d41d..3209a7f27 100644 --- a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go +++ b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go @@ -21,7 +21,7 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/oidcclientsecretstorage" "go.pinniped.dev/internal/plog" ) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index f56e4fc92..0f91d0d68 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -34,8 +34,8 @@ import ( "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" ) @@ -94,7 +94,7 @@ type UpstreamOIDCIdentityProviderICache interface { SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI) } -// lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration. +// lruValidatorCache caches the *coreosoidc.Provider associated with a particular issuer/TLS configuration. type lruValidatorCache struct{ cache *cache.Expiring } type lruValidatorCacheEntry struct { diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index c8e392aef..c6a63698c 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -28,8 +28,8 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -81,7 +81,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) - cache := provider.NewDynamicUpstreamIDPProvider() + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) @@ -1416,7 +1416,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) testLog := testlogger.NewLegacy(t) //nolint:staticcheck // old test with lots of log statements - cache := provider.NewDynamicUpstreamIDPProvider() + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() cache.SetOIDCIdentityProviders([]upstreamprovider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 114a66b5b..0417463fa 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -16,7 +16,7 @@ import ( "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/constable" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) diff --git a/internal/controller/supervisorstorage/garbage_collector.go b/internal/controller/supervisorstorage/garbage_collector.go index 0006f6875..4a736463b 100644 --- a/internal/controller/supervisorstorage/garbage_collector.go +++ b/internal/controller/supervisorstorage/garbage_collector.go @@ -21,13 +21,13 @@ import ( pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/openidconnect" "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestorage/refreshtoken" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) @@ -144,7 +144,7 @@ func (c *garbageCollectorController) Sync(ctx controllerlib.Context) error { // cleaning them out of etcd storage. fourHoursAgo := frozenClock.Now().Add(-4 * time.Hour) nowIsLessThanFourHoursBeyondSecretGCTime := garbageCollectAfterTime.After(fourHoursAgo) - if errors.As(revokeErr, &provider.RetryableRevocationError{}) && nowIsLessThanFourHoursBeyondSecretGCTime { + if errors.As(revokeErr, &dynamicupstreamprovider.RetryableRevocationError{}) && nowIsLessThanFourHoursBeyondSecretGCTime { // Hasn't been very long since secret expired, so skip deletion to try revocation again later. plog.Trace("garbage collector keeping Secret to retry upstream OIDC token revocation later", logKV(secret)...) continue diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index 1e23b1436..79e1c8c0d 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -25,12 +25,12 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/refreshtoken" - "go.pinniped.dev/internal/oidc/clientregistry" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -138,7 +138,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { // Defer starting the informers until the last possible moment so that the // nested Before's can keep adding things to the informer caches. - var startInformersAndController = func(idpCache provider.DynamicUpstreamIDPProvider) { + var startInformersAndController = func(idpCache dynamicupstreamprovider.DynamicUpstreamIDPProvider) { // Set this at the last second to allow for injection of server override. subject = GarbageCollectorController( idpCache, @@ -774,7 +774,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). // make the upstream revocation fail in a retryable way - WithRevokeTokenError(provider.NewRetryableRevocationError(errors.New("some retryable upstream revocation error"))) + WithRevokeTokenError(dynamicupstreamprovider.NewRetryableRevocationError(errors.New("some retryable upstream revocation error"))) idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) diff --git a/internal/oidc/clientregistry/clientregistry.go b/internal/federationdomain/clientregistry/clientregistry.go similarity index 98% rename from internal/oidc/clientregistry/clientregistry.go rename to internal/federationdomain/clientregistry/clientregistry.go index e1d87abb6..7dfa7d9f2 100644 --- a/internal/oidc/clientregistry/clientregistry.go +++ b/internal/federationdomain/clientregistry/clientregistry.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package clientregistry defines Pinniped's OAuth2/OIDC clients. @@ -18,7 +18,7 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" supervisorclient "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/oidcclientsecretstorage" "go.pinniped.dev/internal/plog" ) diff --git a/internal/oidc/clientregistry/clientregistry_test.go b/internal/federationdomain/clientregistry/clientregistry_test.go similarity index 99% rename from internal/oidc/clientregistry/clientregistry_test.go rename to internal/federationdomain/clientregistry/clientregistry_test.go index 036514211..4c33dfccc 100644 --- a/internal/oidc/clientregistry/clientregistry_test.go +++ b/internal/federationdomain/clientregistry/clientregistry_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package clientregistry @@ -21,7 +21,7 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/oidcclientsecretstorage" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/oidc/provider/csp/csp.go b/internal/federationdomain/csp/csp.go similarity index 81% rename from internal/oidc/provider/csp/csp.go rename to internal/federationdomain/csp/csp.go index d3f97e504..8487ca0dd 100644 --- a/internal/oidc/provider/csp/csp.go +++ b/internal/federationdomain/csp/csp.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package csp defines helpers related to HTML Content Security Policies. diff --git a/internal/oidc/provider/csp/csp_test.go b/internal/federationdomain/csp/csp_test.go similarity index 81% rename from internal/oidc/provider/csp/csp_test.go rename to internal/federationdomain/csp/csp_test.go index 746d58220..5fa506977 100644 --- a/internal/oidc/provider/csp/csp_test.go +++ b/internal/federationdomain/csp/csp_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package csp diff --git a/internal/oidc/csrftoken/csrftoken.go b/internal/federationdomain/csrftoken/csrftoken.go similarity index 87% rename from internal/oidc/csrftoken/csrftoken.go rename to internal/federationdomain/csrftoken/csrftoken.go index bc7b713cd..c1d79fe81 100644 --- a/internal/oidc/csrftoken/csrftoken.go +++ b/internal/federationdomain/csrftoken/csrftoken.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package csrftoken diff --git a/internal/oidc/csrftoken/csrftoken_test.go b/internal/federationdomain/csrftoken/csrftoken_test.go similarity index 84% rename from internal/oidc/csrftoken/csrftoken_test.go rename to internal/federationdomain/csrftoken/csrftoken_test.go index 610307314..3c8f6e1ee 100644 --- a/internal/oidc/csrftoken/csrftoken_test.go +++ b/internal/federationdomain/csrftoken/csrftoken_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package csrftoken diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/federationdomain/downstreamsession/downstream_session.go similarity index 99% rename from internal/oidc/downstreamsession/downstream_session.go rename to internal/federationdomain/downstreamsession/downstream_session.go index f40bdff84..6a5f70fe8 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/federationdomain/downstreamsession/downstream_session.go @@ -20,9 +20,9 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/oidctypes" diff --git a/internal/oidc/downstreamsession/downstream_session_test.go b/internal/federationdomain/downstreamsession/downstream_session_test.go similarity index 100% rename from internal/oidc/downstreamsession/downstream_session_test.go rename to internal/federationdomain/downstreamsession/downstream_session_test.go diff --git a/internal/oidc/dynamiccodec/codec.go b/internal/federationdomain/dynamiccodec/codec.go similarity index 93% rename from internal/oidc/dynamiccodec/codec.go rename to internal/federationdomain/dynamiccodec/codec.go index 5168b2b77..36e9a8e92 100644 --- a/internal/oidc/dynamiccodec/codec.go +++ b/internal/federationdomain/dynamiccodec/codec.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package dynamiccodec provides a type that can encode information using a just-in-time signing and @@ -10,7 +10,7 @@ import ( "github.com/gorilla/securecookie" - "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/federationdomain/oidc" ) var _ oidc.Codec = &Codec{} diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/federationdomain/dynamiccodec/codec_test.go similarity index 98% rename from internal/oidc/dynamiccodec/codec_test.go rename to internal/federationdomain/dynamiccodec/codec_test.go index bcff2d038..d6c85357a 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/federationdomain/dynamiccodec/codec_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package dynamiccodec diff --git a/internal/oidc/provider/dynamic_tls_cert_provider.go b/internal/federationdomain/dynamictlscertprovider/dynamic_tls_cert_provider.go similarity index 93% rename from internal/oidc/provider/dynamic_tls_cert_provider.go rename to internal/federationdomain/dynamictlscertprovider/dynamic_tls_cert_provider.go index 7c48ad9c8..a27f2ef4a 100644 --- a/internal/oidc/provider/dynamic_tls_cert_provider.go +++ b/internal/federationdomain/dynamictlscertprovider/dynamic_tls_cert_provider.go @@ -1,7 +1,7 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package provider +package dynamictlscertprovider import ( "crypto/tls" diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go similarity index 97% rename from internal/oidc/provider/dynamic_upstream_idp_provider.go rename to internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go index e9a4333af..bb92e6105 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go @@ -1,13 +1,13 @@ // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package provider +package dynamicupstreamprovider import ( "fmt" "sync" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" ) type DynamicUpstreamIDPProvider interface { diff --git a/internal/oidc/auth/auth_handler.go b/internal/federationdomain/endpoints/auth/auth_handler.go similarity index 98% rename from internal/oidc/auth/auth_handler.go rename to internal/federationdomain/endpoints/auth/auth_handler.go index 3ea3e7ca2..e091a4658 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/federationdomain/endpoints/auth/auth_handler.go @@ -16,17 +16,17 @@ import ( "golang.org/x/oauth2" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/federationdomain/csrftoken" + "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/endpoints/login" + "go.pinniped.dev/internal/federationdomain/federationdomainproviders" + "go.pinniped.dev/internal/federationdomain/formposthtml" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/csrftoken" - "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/login" - "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" - "go.pinniped.dev/internal/oidc/provider/formposthtml" - "go.pinniped.dev/internal/oidc/provider/resolvedprovider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go similarity index 99% rename from internal/oidc/auth/auth_handler_test.go rename to internal/federationdomain/endpoints/auth/auth_handler_test.go index 5a6a1087a..cb4750875 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -30,11 +30,12 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/federationdomain/csrftoken" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/csrftoken" - "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -232,18 +233,18 @@ func TestAuthorizationEndpoint(t *testing.T) { jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() - createOauthHelperWithRealStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *oidc.KubeStorage) { + createOauthHelperWithRealStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *storage.KubeStorage) { // Configure fosite the same way that the production code would when using Kube storage. // Inject this into our test subject at the last second so we get a fresh storage for every test. // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. - kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) + kubeOauthStore := storage.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) return oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration), kubeOauthStore } - createOauthHelperWithNullStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *oidc.NullStorage) { + createOauthHelperWithNullStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *storage.NullStorage) { // Configure fosite the same way that the production code would, using NullStorage to turn off storage. // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. - nullOauthStore := oidc.NewNullStorage(secretsClient, oidcClientsClient, bcrypt.MinCost) + nullOauthStore := storage.NewNullStorage(secretsClient, oidcClientsClient, bcrypt.MinCost) return oidc.FositeOauth2Helper(nullOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration), nullOauthStore } @@ -3175,7 +3176,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, } - runOneTestCase := func(t *testing.T, test testCase, subject http.Handler, kubeOauthStore *oidc.KubeStorage, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset, secretsClient v1.SecretInterface) { + runOneTestCase := func(t *testing.T, test testCase, subject http.Handler, kubeOauthStore *storage.KubeStorage, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset, secretsClient v1.SecretInterface) { if test.kubeResources != nil { test.kubeResources(t, supervisorClient, kubeClient) } diff --git a/internal/oidc/callback/callback_handler.go b/internal/federationdomain/endpoints/callback/callback_handler.go similarity index 95% rename from internal/oidc/callback/callback_handler.go rename to internal/federationdomain/endpoints/callback/callback_handler.go index 5613108ce..8ddb4b2ad 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/federationdomain/endpoints/callback/callback_handler.go @@ -10,12 +10,12 @@ import ( "github.com/ory/fosite" + "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/federationdomainproviders" + "go.pinniped.dev/internal/federationdomain/formposthtml" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" - "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" ) diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go similarity index 99% rename from internal/oidc/callback/callback_handler_test.go rename to internal/federationdomain/endpoints/callback/callback_handler_test.go index 5670ef3f0..15ec387bb 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -21,9 +21,10 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -1451,7 +1452,7 @@ func TestCallbackEndpoint(t *testing.T) { // Inject this into our test subject at the last second so we get a fresh storage for every test. timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. - oauthStore := oidc.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) + oauthStore := storage.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/federationdomain/endpoints/discovery/discovery_handler.go similarity index 98% rename from internal/oidc/discovery/discovery_handler.go rename to internal/federationdomain/endpoints/discovery/discovery_handler.go index 60609c3d3..b7389a84b 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/federationdomain/endpoints/discovery/discovery_handler.go @@ -11,7 +11,7 @@ import ( "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" - "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/federationdomain/oidc" ) // Metadata holds all fields (that we care about) from the OpenID Provider Metadata section in the diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/federationdomain/endpoints/discovery/discovery_handler_test.go similarity index 98% rename from internal/oidc/discovery/discovery_handler_test.go rename to internal/federationdomain/endpoints/discovery/discovery_handler_test.go index 6896b564e..cd205974e 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/federationdomain/endpoints/discovery/discovery_handler_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidc" ) func TestDiscovery(t *testing.T) { diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go similarity index 97% rename from internal/oidc/idpdiscovery/idp_discovery_handler.go rename to internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go index d9034828f..5efb9a577 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go @@ -11,7 +11,7 @@ import ( "sort" "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" - "go.pinniped.dev/internal/oidc/provider/federationdomainproviders" + "go.pinniped.dev/internal/federationdomain/federationdomainproviders" ) // NewHandler returns an http.Handler that serves the upstream IDP discovery endpoint. diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go similarity index 99% rename from internal/oidc/idpdiscovery/idp_discovery_handler_test.go rename to internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go index 9a7fedf24..5257c2543 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/testutil/oidctestutil" ) diff --git a/internal/oidc/jwks/dynamic_jwks_provider.go b/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go similarity index 94% rename from internal/oidc/jwks/dynamic_jwks_provider.go rename to internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go index fa156e3c8..cb8f8e41b 100644 --- a/internal/oidc/jwks/dynamic_jwks_provider.go +++ b/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package jwks diff --git a/internal/oidc/jwks/jwks_handler.go b/internal/federationdomain/endpoints/jwks/jwks_handler.go similarity index 92% rename from internal/oidc/jwks/jwks_handler.go rename to internal/federationdomain/endpoints/jwks/jwks_handler.go index 2c975e958..1d9eb7dfd 100644 --- a/internal/oidc/jwks/jwks_handler.go +++ b/internal/federationdomain/endpoints/jwks/jwks_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package discovery provides a handler for the OIDC discovery endpoint. diff --git a/internal/oidc/jwks/jwks_handler_test.go b/internal/federationdomain/endpoints/jwks/jwks_handler_test.go similarity index 97% rename from internal/oidc/jwks/jwks_handler_test.go rename to internal/federationdomain/endpoints/jwks/jwks_handler_test.go index 37f53e8c9..69d624ce5 100644 --- a/internal/oidc/jwks/jwks_handler_test.go +++ b/internal/federationdomain/endpoints/jwks/jwks_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package jwks diff --git a/internal/oidc/login/get_login_handler.go b/internal/federationdomain/endpoints/login/get_login_handler.go similarity index 84% rename from internal/oidc/login/get_login_handler.go rename to internal/federationdomain/endpoints/login/get_login_handler.go index d6da85a6c..567e6a9e3 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/federationdomain/endpoints/login/get_login_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -6,8 +6,8 @@ package login import ( "net/http" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/endpoints/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/oidc" ) const ( diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/federationdomain/endpoints/login/get_login_handler_test.go similarity index 95% rename from internal/oidc/login/get_login_handler_test.go rename to internal/federationdomain/endpoints/login/get_login_handler_test.go index 30567309c..74405d497 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/get_login_handler_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/idplister" - "go.pinniped.dev/internal/oidc/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/endpoints/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/idplister" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/oidc/login/login_handler.go b/internal/federationdomain/endpoints/login/login_handler.go similarity index 94% rename from internal/oidc/login/login_handler.go rename to internal/federationdomain/endpoints/login/login_handler.go index 1b358f2b4..f7892c70e 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/federationdomain/endpoints/login/login_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -8,11 +8,11 @@ import ( "net/url" idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/internal/federationdomain/endpoints/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/formposthtml" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/login/loginhtml" - "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" ) diff --git a/internal/oidc/login/login_handler_test.go b/internal/federationdomain/endpoints/login/login_handler_test.go similarity index 99% rename from internal/oidc/login/login_handler_test.go rename to internal/federationdomain/endpoints/login/login_handler_test.go index 113809507..854484b35 100644 --- a/internal/oidc/login/login_handler_test.go +++ b/internal/federationdomain/endpoints/login/login_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -13,8 +13,8 @@ import ( "github.com/gorilla/securecookie" "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/httputil/httperr" - "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" ) diff --git a/internal/oidc/login/loginhtml/login_form.css b/internal/federationdomain/endpoints/login/loginhtml/login_form.css similarity index 96% rename from internal/oidc/login/loginhtml/login_form.css rename to internal/federationdomain/endpoints/login/loginhtml/login_form.css index 5eba47e01..19680cc4a 100644 --- a/internal/oidc/login/loginhtml/login_form.css +++ b/internal/federationdomain/endpoints/login/loginhtml/login_form.css @@ -1,4 +1,4 @@ -/* Copyright 2022 the Pinniped contributors. All Rights Reserved. */ +/* Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. */ /* SPDX-License-Identifier: Apache-2.0 */ html { diff --git a/internal/oidc/login/loginhtml/login_form.gohtml b/internal/federationdomain/endpoints/login/loginhtml/login_form.gohtml similarity index 99% rename from internal/oidc/login/loginhtml/login_form.gohtml rename to internal/federationdomain/endpoints/login/loginhtml/login_form.gohtml index c1ab8ba36..7ae90e8cf 100644 --- a/internal/oidc/login/loginhtml/login_form.gohtml +++ b/internal/federationdomain/endpoints/login/loginhtml/login_form.gohtml @@ -1,5 +1,5 @@ diff --git a/internal/oidc/provider/formposthtml/form_post.js b/internal/federationdomain/formposthtml/form_post.js similarity index 98% rename from internal/oidc/provider/formposthtml/form_post.js rename to internal/federationdomain/formposthtml/form_post.js index cb73c8cd4..dcf862755 100644 --- a/internal/oidc/provider/formposthtml/form_post.js +++ b/internal/federationdomain/formposthtml/form_post.js @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 window.onload = () => { diff --git a/internal/oidc/provider/formposthtml/formposthtml.go b/internal/federationdomain/formposthtml/formposthtml.go similarity index 94% rename from internal/oidc/provider/formposthtml/formposthtml.go rename to internal/federationdomain/formposthtml/formposthtml.go index 4fd709c1a..cdf2b85b6 100644 --- a/internal/oidc/provider/formposthtml/formposthtml.go +++ b/internal/federationdomain/formposthtml/formposthtml.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package formposthtml defines HTML templates used by the Supervisor. @@ -11,7 +11,7 @@ import ( "github.com/tdewolff/minify/v2/minify" - "go.pinniped.dev/internal/oidc/provider/csp" + "go.pinniped.dev/internal/federationdomain/csp" ) //nolint:gochecknoglobals // This package uses globals to ensure that all parsing and minifying happens at init. diff --git a/internal/oidc/provider/formposthtml/formposthtml_test.go b/internal/federationdomain/formposthtml/formposthtml_test.go similarity index 99% rename from internal/oidc/provider/formposthtml/formposthtml_test.go rename to internal/federationdomain/formposthtml/formposthtml_test.go index e7d82b75c..3ab3a49a7 100644 --- a/internal/oidc/provider/formposthtml/formposthtml_test.go +++ b/internal/federationdomain/formposthtml/formposthtml_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package formposthtml diff --git a/internal/oidc/idplister/upstream_idp_lister.go b/internal/federationdomain/idplister/upstream_idp_lister.go similarity index 92% rename from internal/oidc/idplister/upstream_idp_lister.go rename to internal/federationdomain/idplister/upstream_idp_lister.go index 3ea4ea649..38b5e27eb 100644 --- a/internal/oidc/idplister/upstream_idp_lister.go +++ b/internal/federationdomain/idplister/upstream_idp_lister.go @@ -4,7 +4,7 @@ package idplister import ( - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" ) type UpstreamOIDCIdentityProvidersLister interface { diff --git a/internal/oidc/oidc.go b/internal/federationdomain/oidc/oidc.go similarity index 70% rename from internal/oidc/oidc.go rename to internal/federationdomain/oidc/oidc.go index 1367f35b3..a314ac68d 100644 --- a/internal/oidc/oidc.go +++ b/internal/federationdomain/oidc/oidc.go @@ -1,7 +1,8 @@ // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package oidc contains common OIDC functionality needed by Pinniped. +// Package oidc contains common OIDC functionality needed by FederationDomains to implement +// downstream OIDC functionality. package oidc import ( @@ -18,10 +19,13 @@ import ( errorsx "github.com/pkg/errors" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/federationdomain/csrftoken" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" + "go.pinniped.dev/internal/federationdomain/endpoints/tokenexchange" + "go.pinniped.dev/internal/federationdomain/formposthtml" + "go.pinniped.dev/internal/federationdomain/strategy" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/httputil/httperr" - "go.pinniped.dev/internal/oidc/csrftoken" - "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -99,81 +103,13 @@ type UpstreamStateParamData struct { FormatVersion string `json:"v"` } -type TimeoutsConfiguration struct { - // The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered - // valid. If a state param generated by the authorize endpoint is sent to the callback endpoint after this much - // time has passed, then the callback endpoint should reject it. This allows us to set a limit on how long - // the end user has to finish their login with the upstream IDP, including the time that it takes to fumble - // with password manager and two-factor authenticator apps, and also accounting for taking a coffee break while - // the browser is sitting at the upstream IDP's login page. - UpstreamStateParamLifespan time.Duration - - // How long an authcode issued by the callback endpoint is valid. This determines how much time the end user - // has to come back to exchange the authcode for tokens at the token endpoint. - AuthorizeCodeLifespan time.Duration - - // The lifetime of an downstream access token issued by the token endpoint. Access tokens should generally - // be fairly short-lived. - AccessTokenLifespan time.Duration - - // The lifetime of an downstream ID token issued by the token endpoint. This should generally be the same - // as the AccessTokenLifespan, or longer if it would be useful for the user's proof of identity to be valid - // for longer than their proof of authorization. - IDTokenLifespan time.Duration - - // The lifetime of an downstream refresh token issued by the token endpoint. This should generally be - // significantly longer than the access token lifetime, so it can be used to refresh the access token - // multiple times. Once the refresh token expires, the user's session is over and they will need - // to start a new authorization request, which will require them to log in again with the upstream IDP - // in their web browser. - RefreshTokenLifespan time.Duration - - // AuthorizationCodeSessionStorageLifetime is the length of time after which an authcode is allowed to be garbage - // collected from storage. Authcodes are kept in storage after they are redeemed to allow the system to mark the - // authcode as already used, so it can reject any future uses of the same authcode with special case handling which - // include revoking the access and refresh tokens associated with the session. Therefore, this should be - // significantly longer than the AuthorizeCodeLifespan, and there is probably no reason to make it longer than - // the sum of the AuthorizeCodeLifespan and the RefreshTokenLifespan. - AuthorizationCodeSessionStorageLifetime time.Duration - - // PKCESessionStorageLifetime is the length of time after which PKCE data is allowed to be garbage collected from - // storage. PKCE sessions are closely related to authorization code sessions. After the authcode is successfully - // redeemed, the PKCE session is explicitly deleted. After the authcode expires, the PKCE session is no longer needed, - // but it is not explicitly deleted. Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll - // avoid making it exactly the same as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it - // while it is being used. - PKCESessionStorageLifetime time.Duration - - // OIDCSessionStorageLifetime is the length of time after which the OIDC session data related to an authcode - // is allowed to be garbage collected from storage. Due to a bug in an underlying library, these are not explicitly - // deleted. Similar to the PKCE session, they are not needed anymore after the corresponding authcode has expired. - // Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll avoid making it exactly the same - // as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it while it is being used. - OIDCSessionStorageLifetime time.Duration - - // AccessTokenSessionStorageLifetime is the length of time after which an access token's session data is allowed - // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid - // or else the refresh flow will not work properly. So this must be longer than RefreshTokenLifespan. - AccessTokenSessionStorageLifetime time.Duration - - // RefreshTokenSessionStorageLifetime is the length of time after which a refresh token's session data is allowed - // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid. - // Therefore, this can be just slightly longer than the RefreshTokenLifespan. We'll avoid making it exactly the same - // as RefreshTokenLifespan to avoid any chance of the garbage collector deleting it while it is being used. - // If an expired token is still stored when the user tries to refresh it, then they will get a more specific - // error message telling them that the token is expired, rather than a more generic error that is returned - // when the token does not exist. If this is desirable, then the RefreshTokenSessionStorageLifetime can be made - // to be significantly larger than RefreshTokenLifespan, at the cost of slower cleanup. - RefreshTokenSessionStorageLifetime time.Duration -} - // Get the defaults for the Supervisor server. -func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration { +func DefaultOIDCTimeoutsConfiguration() timeouts.Configuration { accessTokenLifespan := 2 * time.Minute authorizationCodeLifespan := 10 * time.Minute refreshTokenLifespan := 9 * time.Hour - return TimeoutsConfiguration{ + return timeouts.Configuration{ UpstreamStateParamLifespan: 90 * time.Minute, AuthorizeCodeLifespan: authorizationCodeLifespan, AccessTokenLifespan: accessTokenLifespan, @@ -192,7 +128,7 @@ func FositeOauth2Helper( issuer string, hmacSecretOfLengthAtLeast32Func func() []byte, jwksProvider jwks.DynamicJWKSProvider, - timeoutsConfiguration TimeoutsConfiguration, + timeoutsConfiguration timeouts.Configuration, ) fosite.OAuth2Provider { isRedirectURISecureStrict := func(_ context.Context, uri *url.URL) bool { return fosite.IsRedirectURISecureStrict(uri) @@ -234,15 +170,15 @@ func FositeOauth2Helper( oauthStore, &compose.CommonStrategy{ // Note that Fosite requires the HMAC secret to be at least 32 bytes. - CoreStrategy: newDynamicOauth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32Func), - OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider), + CoreStrategy: strategy.NewDynamicOauth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32Func), + OpenIDConnectTokenStrategy: strategy.NewDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider), }, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2RefreshTokenGrantFactory, compose.OpenIDConnectExplicitFactory, compose.OpenIDConnectRefreshFactory, compose.OAuth2PKCEFactory, - TokenExchangeFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type + tokenexchange.HandlerFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type ) return oAuth2Provider diff --git a/internal/oidc/oidcclientvalidator/oidcclientvalidator.go b/internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go similarity index 100% rename from internal/oidc/oidcclientvalidator/oidcclientvalidator.go rename to internal/federationdomain/oidcclientvalidator/oidcclientvalidator.go diff --git a/internal/oidc/provider/resolvedprovider/resolved_provider.go b/internal/federationdomain/resolvedprovider/resolved_provider.go similarity index 87% rename from internal/oidc/provider/resolvedprovider/resolved_provider.go rename to internal/federationdomain/resolvedprovider/resolved_provider.go index f37f0a161..e2b43a869 100644 --- a/internal/oidc/provider/resolvedprovider/resolved_provider.go +++ b/internal/federationdomain/resolvedprovider/resolved_provider.go @@ -1,8 +1,11 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package resolvedprovider import ( + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" ) diff --git a/internal/oidc/dynamic_global_secret_config.go b/internal/federationdomain/storage/dynamic_global_secret_config.go similarity index 96% rename from internal/oidc/dynamic_global_secret_config.go rename to internal/federationdomain/storage/dynamic_global_secret_config.go index b9001eeaa..50f997c4c 100644 --- a/internal/oidc/dynamic_global_secret_config.go +++ b/internal/federationdomain/storage/dynamic_global_secret_config.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package storage import ( "context" diff --git a/internal/oidc/kube_storage.go b/internal/federationdomain/storage/kube_storage.go similarity index 97% rename from internal/oidc/kube_storage.go rename to internal/federationdomain/storage/kube_storage.go index a197335e9..e10ed60e3 100644 --- a/internal/oidc/kube_storage.go +++ b/internal/federationdomain/storage/kube_storage.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package storage import ( "context" @@ -14,13 +14,14 @@ import ( corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" + "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/openidconnect" "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestorage/refreshtoken" "go.pinniped.dev/internal/fositestoragei" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidcclientsecretstorage" ) @@ -38,7 +39,7 @@ var _ fositestoragei.AllFositeStorage = &KubeStorage{} func NewKubeStorage( secrets corev1client.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface, - timeoutsConfiguration TimeoutsConfiguration, + timeoutsConfiguration timeouts.Configuration, minBcryptCost int, ) *KubeStorage { nowFunc := time.Now diff --git a/internal/oidc/nullstorage.go b/internal/federationdomain/storage/null_storage.go similarity index 96% rename from internal/oidc/nullstorage.go rename to internal/federationdomain/storage/null_storage.go index 61476f811..ae5b0ecc8 100644 --- a/internal/oidc/nullstorage.go +++ b/internal/federationdomain/storage/null_storage.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package storage import ( "context" @@ -11,8 +11,8 @@ import ( "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestoragei" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidcclientsecretstorage" ) diff --git a/internal/oidc/dynamic_oauth2_hmac_strategy.go b/internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy.go similarity index 81% rename from internal/oidc/dynamic_oauth2_hmac_strategy.go rename to internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy.go index d5456b673..200dd41c9 100644 --- a/internal/oidc/dynamic_oauth2_hmac_strategy.go +++ b/internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package strategy import ( "context" @@ -11,6 +11,8 @@ import ( "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/oauth2" errorsx "github.com/pkg/errors" + + "go.pinniped.dev/internal/federationdomain/storage" ) const ( @@ -24,7 +26,7 @@ const ( oryAuthcodePrefix = "ory_ac_" ) -// dynamicOauth2HMACStrategy is an oauth2.CoreStrategy that can dynamically load an HMAC key to sign +// DynamicOauth2HMACStrategy is an oauth2.CoreStrategy that can dynamically load an HMAC key to sign // stuff (access tokens, refresh tokens, and auth codes). We want this dynamic capability since our // controllers for loading FederationDomain's and signing keys run in parallel, and thus the signing key // might not be ready when an FederationDomain is otherwise ready. @@ -37,18 +39,18 @@ const ( // out of context, such as when accidentally committed to a GitHub repo. After we implemented the // custom prefix feature, fosite later added the same feature, but did not make the prefix customizable. // Therefore, this code has been updated to replace the fosite prefix with our custom prefix. -type dynamicOauth2HMACStrategy struct { +type DynamicOauth2HMACStrategy struct { fositeConfig *fosite.Config keyFunc func() []byte } -var _ oauth2.CoreStrategy = &dynamicOauth2HMACStrategy{} +var _ oauth2.CoreStrategy = &DynamicOauth2HMACStrategy{} -func newDynamicOauth2HMACStrategy( +func NewDynamicOauth2HMACStrategy( fositeConfig *fosite.Config, keyFunc func() []byte, -) *dynamicOauth2HMACStrategy { - return &dynamicOauth2HMACStrategy{ +) *DynamicOauth2HMACStrategy { + return &DynamicOauth2HMACStrategy{ fositeConfig: fositeConfig, keyFunc: keyFunc, } @@ -58,11 +60,11 @@ func replacePrefix(s, prefixToReplace, newPrefix string) string { return newPrefix + strings.TrimPrefix(s, prefixToReplace) } -func (s *dynamicOauth2HMACStrategy) AccessTokenSignature(ctx context.Context, token string) string { +func (s *DynamicOauth2HMACStrategy) AccessTokenSignature(ctx context.Context, token string) string { return s.delegate().AccessTokenSignature(ctx, token) } -func (s *dynamicOauth2HMACStrategy) GenerateAccessToken( +func (s *DynamicOauth2HMACStrategy) GenerateAccessToken( ctx context.Context, requester fosite.Requester, ) (string, string, error) { @@ -78,7 +80,7 @@ func (s *dynamicOauth2HMACStrategy) GenerateAccessToken( return token, sig, err } -func (s *dynamicOauth2HMACStrategy) ValidateAccessToken( +func (s *DynamicOauth2HMACStrategy) ValidateAccessToken( ctx context.Context, requester fosite.Requester, token string, @@ -90,11 +92,11 @@ func (s *dynamicOauth2HMACStrategy) ValidateAccessToken( return s.delegate().ValidateAccessToken(ctx, requester, replacePrefix(token, pinAccessTokenPrefix, oryAccessTokenPrefix)) } -func (s *dynamicOauth2HMACStrategy) RefreshTokenSignature(ctx context.Context, token string) string { +func (s *DynamicOauth2HMACStrategy) RefreshTokenSignature(ctx context.Context, token string) string { return s.delegate().RefreshTokenSignature(ctx, token) } -func (s *dynamicOauth2HMACStrategy) GenerateRefreshToken( +func (s *DynamicOauth2HMACStrategy) GenerateRefreshToken( ctx context.Context, requester fosite.Requester, ) (string, string, error) { @@ -110,7 +112,7 @@ func (s *dynamicOauth2HMACStrategy) GenerateRefreshToken( return token, sig, err } -func (s *dynamicOauth2HMACStrategy) ValidateRefreshToken( +func (s *DynamicOauth2HMACStrategy) ValidateRefreshToken( ctx context.Context, requester fosite.Requester, token string, @@ -122,11 +124,11 @@ func (s *dynamicOauth2HMACStrategy) ValidateRefreshToken( return s.delegate().ValidateRefreshToken(ctx, requester, replacePrefix(token, pinRefreshTokenPrefix, oryRefreshTokenPrefix)) } -func (s *dynamicOauth2HMACStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { +func (s *DynamicOauth2HMACStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { return s.delegate().AuthorizeCodeSignature(ctx, token) } -func (s *dynamicOauth2HMACStrategy) GenerateAuthorizeCode( +func (s *DynamicOauth2HMACStrategy) GenerateAuthorizeCode( ctx context.Context, requester fosite.Requester, ) (string, string, error) { @@ -142,7 +144,7 @@ func (s *dynamicOauth2HMACStrategy) GenerateAuthorizeCode( return authcode, sig, err } -func (s *dynamicOauth2HMACStrategy) ValidateAuthorizeCode( +func (s *DynamicOauth2HMACStrategy) ValidateAuthorizeCode( ctx context.Context, requester fosite.Requester, token string, @@ -154,6 +156,6 @@ func (s *dynamicOauth2HMACStrategy) ValidateAuthorizeCode( return s.delegate().ValidateAuthorizeCode(ctx, requester, replacePrefix(token, pinAuthcodePrefix, oryAuthcodePrefix)) } -func (s *dynamicOauth2HMACStrategy) delegate() *oauth2.HMACSHAStrategy { - return compose.NewOAuth2HMACStrategy(NewDynamicGlobalSecretConfig(s.fositeConfig, s.keyFunc)) +func (s *DynamicOauth2HMACStrategy) delegate() *oauth2.HMACSHAStrategy { + return compose.NewOAuth2HMACStrategy(storage.NewDynamicGlobalSecretConfig(s.fositeConfig, s.keyFunc)) } diff --git a/internal/oidc/dynamic_oauth2_hmac_strategy_test.go b/internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy_test.go similarity index 97% rename from internal/oidc/dynamic_oauth2_hmac_strategy_test.go rename to internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy_test.go index c2028a79d..02bf861c3 100644 --- a/internal/oidc/dynamic_oauth2_hmac_strategy_test.go +++ b/internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy_test.go @@ -1,7 +1,7 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package strategy import ( "context" @@ -15,7 +15,7 @@ import ( ) func TestDynamicOauth2HMACStrategy_Signatures(t *testing.T) { - s := newDynamicOauth2HMACStrategy( + s := NewDynamicOauth2HMACStrategy( &fosite.Config{}, // defaults are good enough for this unit test func() []byte { return []byte("12345678901234567890123456789012") }, // 32 character secret key ) @@ -57,12 +57,12 @@ func TestDynamicOauth2HMACStrategy_Signatures(t *testing.T) { } func TestDynamicOauth2HMACStrategy_Generate(t *testing.T) { - s := newDynamicOauth2HMACStrategy( + s := NewDynamicOauth2HMACStrategy( &fosite.Config{}, // defaults are good enough for this unit test func() []byte { return []byte("12345678901234567890123456789012") }, // 32 character secret key ) - generateTokenErrorCausingStrategy := newDynamicOauth2HMACStrategy( + generateTokenErrorCausingStrategy := NewDynamicOauth2HMACStrategy( &fosite.Config{}, func() []byte { return []byte("too_short_causes_error") }, // secret key is below required 32 characters ) @@ -134,7 +134,7 @@ func TestDynamicOauth2HMACStrategy_Generate(t *testing.T) { } func TestDynamicOauth2HMACStrategy_Validate(t *testing.T) { - s := newDynamicOauth2HMACStrategy( + s := NewDynamicOauth2HMACStrategy( &fosite.Config{}, // defaults are good enough for this unit test func() []byte { return []byte("12345678901234567890123456789012") }, // 32 character secret key ) diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy.go similarity index 79% rename from internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go rename to internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy.go index 807828823..463a450c4 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go +++ b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package strategy import ( "context" @@ -14,11 +14,11 @@ import ( "github.com/ory/fosite/handler/openid" "go.pinniped.dev/internal/constable" - "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/plog" ) -// dynamicOpenIDConnectECDSAStrategy is an openid.OpenIDConnectTokenStrategy that can dynamically +// DynamicOpenIDConnectECDSAStrategy is an openid.OpenIDConnectTokenStrategy that can dynamically // load a signing key to issue ID tokens. We want this dynamic capability since our controllers for // loading FederationDomain's and signing keys run in parallel, and thus the signing key might not be // ready when an FederationDomain is otherwise ready. @@ -26,24 +26,24 @@ import ( // If we ever update FederationDomain's to hold their signing key, we might not need this type, since we // could have an invariant that routes to an FederationDomain's endpoints are only wired up if an // FederationDomain has a valid signing key. -type dynamicOpenIDConnectECDSAStrategy struct { +type DynamicOpenIDConnectECDSAStrategy struct { fositeConfig *fosite.Config jwksProvider jwks.DynamicJWKSProvider } -var _ openid.OpenIDConnectTokenStrategy = &dynamicOpenIDConnectECDSAStrategy{} +var _ openid.OpenIDConnectTokenStrategy = &DynamicOpenIDConnectECDSAStrategy{} -func newDynamicOpenIDConnectECDSAStrategy( +func NewDynamicOpenIDConnectECDSAStrategy( fositeConfig *fosite.Config, jwksProvider jwks.DynamicJWKSProvider, -) *dynamicOpenIDConnectECDSAStrategy { - return &dynamicOpenIDConnectECDSAStrategy{ +) *DynamicOpenIDConnectECDSAStrategy { + return &DynamicOpenIDConnectECDSAStrategy{ fositeConfig: fositeConfig, jwksProvider: jwksProvider, } } -func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken( +func (s *DynamicOpenIDConnectECDSAStrategy) GenerateIDToken( ctx context.Context, lifespan time.Duration, requester fosite.Requester, diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go similarity index 95% rename from internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go rename to internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go index 3573f9872..94bf676a7 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go +++ b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go @@ -1,7 +1,7 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package oidc +package strategy import ( "context" @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" - "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/testutil/oidctestutil" ) @@ -94,7 +94,7 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { if test.jwksProvider != nil { test.jwksProvider(jwksProvider) } - s := newDynamicOpenIDConnectECDSAStrategy( + s := NewDynamicOpenIDConnectECDSAStrategy( &fosite.Config{IDTokenIssuer: test.issuer}, jwksProvider, ) diff --git a/internal/federationdomain/timeouts/timeouts_configuration.go b/internal/federationdomain/timeouts/timeouts_configuration.go new file mode 100644 index 000000000..9657eaa49 --- /dev/null +++ b/internal/federationdomain/timeouts/timeouts_configuration.go @@ -0,0 +1,74 @@ +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package timeouts + +import "time" + +type Configuration struct { + // The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered + // valid. If a state param generated by the authorize endpoint is sent to the callback endpoint after this much + // time has passed, then the callback endpoint should reject it. This allows us to set a limit on how long + // the end user has to finish their login with the upstream IDP, including the time that it takes to fumble + // with password manager and two-factor authenticator apps, and also accounting for taking a coffee break while + // the browser is sitting at the upstream IDP's login page. + UpstreamStateParamLifespan time.Duration + + // How long an authcode issued by the callback endpoint is valid. This determines how much time the end user + // has to come back to exchange the authcode for tokens at the token endpoint. + AuthorizeCodeLifespan time.Duration + + // The lifetime of an downstream access token issued by the token endpoint. Access tokens should generally + // be fairly short-lived. + AccessTokenLifespan time.Duration + + // The lifetime of an downstream ID token issued by the token endpoint. This should generally be the same + // as the AccessTokenLifespan, or longer if it would be useful for the user's proof of identity to be valid + // for longer than their proof of authorization. + IDTokenLifespan time.Duration + + // The lifetime of an downstream refresh token issued by the token endpoint. This should generally be + // significantly longer than the access token lifetime, so it can be used to refresh the access token + // multiple times. Once the refresh token expires, the user's session is over and they will need + // to start a new authorization request, which will require them to log in again with the upstream IDP + // in their web browser. + RefreshTokenLifespan time.Duration + + // AuthorizationCodeSessionStorageLifetime is the length of time after which an authcode is allowed to be garbage + // collected from storage. Authcodes are kept in storage after they are redeemed to allow the system to mark the + // authcode as already used, so it can reject any future uses of the same authcode with special case handling which + // include revoking the access and refresh tokens associated with the session. Therefore, this should be + // significantly longer than the AuthorizeCodeLifespan, and there is probably no reason to make it longer than + // the sum of the AuthorizeCodeLifespan and the RefreshTokenLifespan. + AuthorizationCodeSessionStorageLifetime time.Duration + + // PKCESessionStorageLifetime is the length of time after which PKCE data is allowed to be garbage collected from + // storage. PKCE sessions are closely related to authorization code sessions. After the authcode is successfully + // redeemed, the PKCE session is explicitly deleted. After the authcode expires, the PKCE session is no longer needed, + // but it is not explicitly deleted. Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll + // avoid making it exactly the same as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it + // while it is being used. + PKCESessionStorageLifetime time.Duration + + // OIDCSessionStorageLifetime is the length of time after which the OIDC session data related to an authcode + // is allowed to be garbage collected from storage. Due to a bug in an underlying library, these are not explicitly + // deleted. Similar to the PKCE session, they are not needed anymore after the corresponding authcode has expired. + // Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll avoid making it exactly the same + // as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it while it is being used. + OIDCSessionStorageLifetime time.Duration + + // AccessTokenSessionStorageLifetime is the length of time after which an access token's session data is allowed + // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid + // or else the refresh flow will not work properly. So this must be longer than RefreshTokenLifespan. + AccessTokenSessionStorageLifetime time.Duration + + // RefreshTokenSessionStorageLifetime is the length of time after which a refresh token's session data is allowed + // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid. + // Therefore, this can be just slightly longer than the RefreshTokenLifespan. We'll avoid making it exactly the same + // as RefreshTokenLifespan to avoid any chance of the garbage collector deleting it while it is being used. + // If an expired token is still stored when the user tries to refresh it, then they will get a more specific + // error message telling them that the token is expired, rather than a more generic error that is returned + // when the token does not exist. If this is desirable, then the RefreshTokenSessionStorageLifetime can be made + // to be significantly larger than RefreshTokenLifespan, at the cost of slower cleanup. + RefreshTokenSessionStorageLifetime time.Duration +} diff --git a/internal/oidc/provider/upstreamprovider/upsteam_provider.go b/internal/federationdomain/upstreamprovider/upsteam_provider.go similarity index 100% rename from internal/oidc/provider/upstreamprovider/upsteam_provider.go rename to internal/federationdomain/upstreamprovider/upsteam_provider.go diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index 07b065ed4..042478898 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -16,8 +16,8 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index 52a731f30..2e571e9af 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -22,7 +22,7 @@ import ( coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index 6c451acfd..c2fe859d9 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -17,8 +17,8 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index 355800018..69912765f 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -34,8 +34,8 @@ import ( kubetesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/fositestorage/fositestorage.go b/internal/fositestorage/fositestorage.go index af99caed7..c15c8c90e 100644 --- a/internal/fositestorage/fositestorage.go +++ b/internal/fositestorage/fositestorage.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package fositestorage @@ -7,7 +7,7 @@ import ( "github.com/ory/fosite" "go.pinniped.dev/internal/constable" - "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index 4770e41f6..c2aa553d8 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -16,8 +16,8 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index e4740ac73..e278deeae 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -21,7 +21,7 @@ import ( coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index 3f44a00d3..6cea9a851 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -15,8 +15,8 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index f0a24fd4a..bc424593c 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -21,7 +21,7 @@ import ( coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index 9feaed559..d3abdc4f0 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -16,8 +16,8 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/psession" ) diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index 8e2826b9e..24da03a1f 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -22,7 +22,7 @@ import ( coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) diff --git a/internal/mocks/issuermocks/generate.go b/internal/mocks/issuermocks/generate.go index 7d0c9937e..c770c9275 100644 --- a/internal/mocks/issuermocks/generate.go +++ b/internal/mocks/issuermocks/generate.go @@ -1,6 +1,6 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package issuermocks -//go:generate go run -v github.com/golang/mock/mockgen -destination=issuermocks.go -package=issuermocks -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/issuer ClientCertIssuer +//go:generate go run -v github.com/golang/mock/mockgen -destination=issuermocks.go -package=issuermocks -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/clientcertissuer ClientCertIssuer diff --git a/internal/mocks/issuermocks/issuermocks.go b/internal/mocks/issuermocks/issuermocks.go index 6867a2a0e..2edd28481 100644 --- a/internal/mocks/issuermocks/issuermocks.go +++ b/internal/mocks/issuermocks/issuermocks.go @@ -3,7 +3,7 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: go.pinniped.dev/internal/issuer (interfaces: ClientCertIssuer) +// Source: go.pinniped.dev/internal/clientcertissuer (interfaces: ClientCertIssuer) // Package issuermocks is a generated GoMock package. package issuermocks diff --git a/internal/mocks/mockupstreamoidcidentityprovider/generate.go b/internal/mocks/mockupstreamoidcidentityprovider/generate.go index d1ba84a07..e94a53380 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/generate.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/generate.go @@ -3,4 +3,4 @@ package mockupstreamoidcidentityprovider -//go:generate go run -v github.com/golang/mock/mockgen -destination=mockupstreamoidcidentityprovider.go -package=mockupstreamoidcidentityprovider -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/oidc/provider/upstreamprovider UpstreamOIDCIdentityProviderI +//go:generate go run -v github.com/golang/mock/mockgen -destination=mockupstreamoidcidentityprovider.go -package=mockupstreamoidcidentityprovider -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/federationdomain/upstreamprovider UpstreamOIDCIdentityProviderI diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index cc66519f6..cad36a261 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -3,7 +3,7 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: go.pinniped.dev/internal/oidc/provider/upstreamprovider (interfaces: UpstreamOIDCIdentityProviderI) +// Source: go.pinniped.dev/internal/federationdomain/upstreamprovider (interfaces: UpstreamOIDCIdentityProviderI) // Package mockupstreamoidcidentityprovider is a generated GoMock package. package mockupstreamoidcidentityprovider @@ -14,7 +14,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - upstreamprovider "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + upstreamprovider "go.pinniped.dev/internal/federationdomain/upstreamprovider" nonce "go.pinniped.dev/pkg/oidcclient/nonce" oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes" pkce "go.pinniped.dev/pkg/oidcclient/pkce" diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 5d67c2c30..5018038f2 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -58,12 +58,13 @@ import ( "go.pinniped.dev/internal/deploymentref" "go.pinniped.dev/internal/downward" "go.pinniped.dev/internal/dynamiccert" + "go.pinniped.dev/internal/federationdomain/dynamictlscertprovider" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/endpoints/jwks" + "go.pinniped.dev/internal/federationdomain/endpointsmanager" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/leaderelection" - "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/manager" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/pversion" "go.pinniped.dev/internal/secret" @@ -129,10 +130,10 @@ func signalCtx() context.Context { //nolint:funlen func prepareControllers( cfg *supervisor.Config, - issuerManager *manager.Manager, + issuerManager *endpointsmanager.Manager, dynamicJWKSProvider jwks.DynamicJWKSProvider, - dynamicTLSCertProvider provider.DynamicTLSCertProvider, - dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider, + dynamicTLSCertProvider dynamictlscertprovider.DynamicTLSCertProvider, + dynamicUpstreamIDPProvider dynamicupstreamprovider.DynamicUpstreamIDPProvider, dynamicServingCertProvider dynamiccert.Private, secretCache *secret.Cache, supervisorDeployment *appsv1.Deployment, @@ -436,12 +437,12 @@ func runSupervisor(ctx context.Context, podInfo *downward.PodInfo, cfg *supervis dynamicServingCertProvider := dynamiccert.NewServingCert("supervisor-serving-cert") dynamicJWKSProvider := jwks.NewDynamicJWKSProvider() - dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider() - dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider() + dynamicTLSCertProvider := dynamictlscertprovider.NewDynamicTLSCertProvider() + dynamicUpstreamIDPProvider := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() secretCache := secret.Cache{} // OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux. - oidProvidersManager := manager.NewManager( + oidProvidersManager := endpointsmanager.NewManager( healthMux, dynamicJWKSProvider, dynamicUpstreamIDPProvider, diff --git a/internal/testutil/oidcclient_test.go b/internal/testutil/oidcclient_test.go index cd8923137..c47a129a0 100644 --- a/internal/testutil/oidcclient_test.go +++ b/internal/testutil/oidcclient_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil @@ -6,10 +6,10 @@ package testutil import ( "testing" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" - "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" + + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" ) func TestBcryptConstants(t *testing.T) { diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index e879ffcf3..46aab19ac 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -30,19 +30,19 @@ import ( "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/openidconnect" - pkce2 "go.pinniped.dev/internal/fositestorage/pkce" + "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/resolvedprovider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" - "go.pinniped.dev/pkg/oidcclient/pkce" + oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" ) // Test helpers for the OIDC package. @@ -52,7 +52,7 @@ import ( type ExchangeAuthcodeAndValidateTokenArgs struct { Ctx context.Context Authcode string - PKCECodeVerifier pkce.Code + PKCECodeVerifier oidcpkce.Code ExpectedIDTokenNonce nonce.Nonce RedirectURI string } @@ -267,7 +267,7 @@ type TestUpstreamOIDCIdentityProvider struct { ExchangeAuthcodeAndValidateTokensFunc func( ctx context.Context, authcode string, - pkceCodeVerifier pkce.Code, + pkceCodeVerifier oidcpkce.Code, expectedIDTokenNonce nonce.Nonce, ) (*oidctypes.Token, error) @@ -358,7 +358,7 @@ func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTo func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens( ctx context.Context, authcode string, - pkceCodeVerifier pkce.Code, + pkceCodeVerifier oidcpkce.Code, expectedIDTokenNonce nonce.Nonce, redirectURI string, ) (*oidctypes.Token, error) { @@ -595,8 +595,8 @@ func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerF } } -func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() provider.DynamicUpstreamIDPProvider { - idpProvider := provider.NewDynamicUpstreamIDPProvider() +func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() dynamicupstreamprovider.DynamicUpstreamIDPProvider { + idpProvider := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() oidcUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) for i := range b.upstreamOIDCIdentityProviders { @@ -1038,7 +1038,7 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent AdditionalClaimMappings: u.additionalClaimMappings, DisplayNameForFederationDomain: u.displayNameForFederationDomain, TransformsForFederationDomain: u.transformsForFederationDomain, - ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { + ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier oidcpkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { if u.authcodeExchangeErr != nil { return nil, u.authcodeExchangeErr } @@ -1223,7 +1223,7 @@ func RequireAuthCodeRegexpMatch( ) // One PKCE should have been stored. - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: pkce2.TypeLabelValue}, 1) + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: pkce.TypeLabelValue}, 1) validatePKCEStorage( t, diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 7dd0dcc67..b178adf2e 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -27,8 +27,8 @@ import ( "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" - "go.pinniped.dev/internal/oidc/downstreamsession" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" ) diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 02476b253..d07c2c9e1 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -25,8 +25,8 @@ import ( "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/mocks/mockldapconn" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/tlsassertions" "go.pinniped.dev/internal/testutil/tlsserver" diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index dc61b11f4..8cd569a18 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -21,9 +21,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/httperr" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -221,7 +221,7 @@ func (p *ProviderConfig) tryRevokeToken( if err != nil { // Couldn't connect to the server or some similar error. // Could be a temporary network problem, so it might be worth retrying. - return false, provider.NewRetryableRevocationError(err) + return false, dynamicupstreamprovider.NewRetryableRevocationError(err) } defer resp.Body.Close() @@ -271,7 +271,7 @@ func (p *ProviderConfig) tryRevokeToken( // be caused by an underlying problem which could potentially become resolved in the near future. We'll be // optimistic and call all 5xx errors retryable. plog.Trace("RevokeToken() got unexpected error response from provider's revocation endpoint", "providerName", p.Name, "usedBasicAuth", useBasicAuth, "statusCode", status) - return false, provider.NewRetryableRevocationError(fmt.Errorf("server responded with status %d", status)) + return false, dynamicupstreamprovider.NewRetryableRevocationError(fmt.Errorf("server responded with status %d", status)) default: // Any other error is probably not due to failed client auth, and is probably not worth retrying later. plog.Trace("RevokeToken() got unexpected error response from provider's revocation endpoint", "providerName", p.Name, "usedBasicAuth", useBasicAuth, "statusCode", status) diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index e477f1ba6..cc4f06a23 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -23,9 +23,9 @@ import ( "gopkg.in/square/go-jose.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/mocks/mockkeyset" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -715,8 +715,8 @@ func TestProviderConfig(t *testing.T) { testutil.RequireErrorStringFromErr(t, err, tt.wantErr) if tt.wantRetryableErrType { - require.ErrorAs(t, err, &provider.RetryableRevocationError{}) - } else if errors.As(err, &provider.RetryableRevocationError{}) { + require.ErrorAs(t, err, &dynamicupstreamprovider.RetryableRevocationError{}) + } else if errors.As(err, &dynamicupstreamprovider.RetryableRevocationError{}) { // There is no NotErrorAs() assertion available in the current version of testify, so do the equivalent. require.Fail(t, "error should not be As RetryableRevocationError") } diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 341d1ffd9..fac2b99c4 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -30,10 +30,10 @@ import ( "k8s.io/utils/strings/slices" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/pkg/oidcclient/nonce" diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index 871d60141..b041dbfcf 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -28,11 +28,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" "go.pinniped.dev/internal/net/phttp" - "go.pinniped.dev/internal/oidc/provider/upstreamprovider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 1a204c454..090a3c543 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -160,7 +160,7 @@ Once dynamic clients are implemented: #### New Dependencies This should be kept to a very simple HTML page with minimal, clean CSS styling. Javascript should be avoided. -The styling should match the [form post html page](https://github.com/vmware-tanzu/pinniped/tree/main/internal/oidc/provider/formposthtml) +The styling should match the [form post html page](https://github.com/vmware-tanzu/pinniped/tree/main/internal/federationdomain/formposthtml) as much as possible, we should reuse some of the existing css and add to it to keep the style consistent. #### Observability Considerations diff --git a/site/content/docs/reference/code-walkthrough.md b/site/content/docs/reference/code-walkthrough.md index 11c84cd7a..bd18b6f91 100644 --- a/site/content/docs/reference/code-walkthrough.md +++ b/site/content/docs/reference/code-walkthrough.md @@ -192,28 +192,28 @@ The Supervisor's endpoints are: Each FederationDomain's endpoints are mounted under the path of the FederationDomain's `spec.issuer`, if the `spec.issuer` URL has a path component specified. If the issuer has no path, then they are mounted under `/`. These per-FederationDomain endpoint are all mounted by the code in -[internal/oidc/provider/manager/manager.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/provider/manager/manager.go). +[internal/federationdomain/endpointsmanager/manager.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpointsmanager/manager.go). The per-FederationDomain endpoints are: - `/.well-known/openid-configuration` is the standard OIDC discovery endpoint, which can be used to discover all the other endpoints listed here. - See [internal/oidc/discovery/discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/discovery/discovery_handler.go). + See [internal/federationdomain/endpoints/discovery/discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/discovery/discovery_handler.go). - `/jwks.json` is the standard OIDC JWKS discovery endpoint. - See [internal/oidc/jwks/jwks_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/jwks/jwks_handler.go). + See [internal/federationdomain/endpoints/jwks/jwks_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/jwks/jwks_handler.go). - `/oauth2/authorize` is the standard OIDC authorize endpoint. - See [internal/oidc/auth/auth_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/auth/auth_handler.go). + See [internal/federationdomain/endpoints/auth/auth_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/auth/auth_handler.go). - `/oauth2/token` is the standard OIDC token endpoint. - See [internal/oidc/token/token_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/token/token_handler.go). + See [internal/federationdomain/endpoints/token/token_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/token/token_handler.go). The token endpoint can handle the standard OIDC `authorization_code` and `refresh_token` grant types, and has also been - extended in [internal/oidc/token_exchange.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/token_exchange.go) + extended in [internal/federationdomain/endpoints/tokenexchange/token_exchange.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/tokenexchange/token_exchange.go) to handle an additional grant type for [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchanges to reduce the applicable scope (technically, the `aud` claim) of ID tokens. - `/callback` is a special endpoint that is used as the redirect URL when performing an OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider custom resource. - See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go). + See [internal/federationdomain/endpoints/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/callback/callback_handler.go). - `/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers. - See [internal/oidc/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/idpdiscovery/idp_discovery_handler.go). + See [internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go). - `/login` is a login UI page to support the optional browser-based login flow for LDAP and Active Directory identity providers. - See [internal/oidc/login/login_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/login/login_handler.go). + See [internal/federationdomain/endpoints/login/login_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/login/login_handler.go). The OIDC specifications implemented by the Supervisor can be found at [openid.net](https://openid.net/connect). diff --git a/test/integration/formposthtml_test.go b/test/integration/formposthtml_test.go index 1a3b45299..9056e065b 100644 --- a/test/integration/formposthtml_test.go +++ b/test/integration/formposthtml_test.go @@ -19,8 +19,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/federationdomain/formposthtml" "go.pinniped.dev/internal/httputil/securityheader" - "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/test/testlib" "go.pinniped.dev/test/testlib/browsertest" ) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index ae58aead9..d424014c0 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -31,8 +31,9 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/certauthority" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -2240,7 +2241,7 @@ func testSupervisorLogin( // First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage. supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace) supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace) - oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) + oauthStore := storage.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil) require.NoError(t, err) @@ -2302,7 +2303,7 @@ func testSupervisorLogin( // First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage. supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace) supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace) - oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) + oauthStore := storage.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil) require.NoError(t, err) diff --git a/test/integration/supervisor_storage_test.go b/test/integration/supervisor_storage_test.go index e58322622..d56cc6be1 100644 --- a/test/integration/supervisor_storage_test.go +++ b/test/integration/supervisor_storage_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -17,8 +17,8 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/fositestorage/authorizationcode" - "go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/test/testlib" ) diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index f84b5358d..5c93e687b 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -28,8 +28,9 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/certauthority" - "go.pinniped.dev/internal/oidc" - "go.pinniped.dev/internal/oidc/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" + "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient/filesession" @@ -186,7 +187,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { // out of kube secret storage. supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace) supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace) - oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) + oauthStore := storage.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1] storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil) require.NoError(t, err) @@ -494,7 +495,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { // out of kube secret storage. supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace) supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace) - oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) + oauthStore := storage.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost) refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1] storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil) require.NoError(t, err) From 9d792352bf15f5696eea16b011759d8936a7eef6 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 26 Jun 2023 13:46:28 -0400 Subject: [PATCH 16/81] test FederationDomainIdentityProvidersListerFinder Co-authored-by: Ryan Richard --- ...domain_identity_providers_lister_finder.go | 4 +- ...n_identity_providers_lister_finder_test.go | 535 ++++++++++++++++++ 2 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go index ab1d9fd93..00471b732 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package federationdomainproviders @@ -128,7 +128,7 @@ func (u *FederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplay return nil, p, nil } } - return nil, nil, fmt.Errorf("identity provider not found: %q", upstreamIDPDisplayName) + return nil, nil, fmt.Errorf("identity provider not available: %q", upstreamIDPDisplayName) } // FindDefaultIDP works like FindUpstreamIDPByDisplayName, but finds the default IDP instead of finding by name. diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go new file mode 100644 index 000000000..5369f3dd5 --- /dev/null +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go @@ -0,0 +1,535 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package federationdomainproviders + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/federationdomain/idplister" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { + // IdPs + myDefaultOIDCIDP := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName("my-default-oidc-idp"). + WithResourceUID("my-default-oidc-uid-idp"). + Build() + myOIDCIDP1 := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName("my-oidc-idp1"). + WithResourceUID("my-oidc-uid-idp1"). + Build() + myOIDCIDP2 := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName("my-oidc-idp2"). + WithResourceUID("my-oidc-uid-idp2"). + Build() + + myDefaultLDAPIDP := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-default-ldap-idp"). + WithResourceUID("my-default-ldap-uid-idp"). + Build() + myLDAPIDP1 := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ldap-idp1"). + WithResourceUID("my-ldap-uid-idp1"). + Build() + myLDAPIDP2 := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ldap-idp2"). + WithResourceUID("my-ldap-uid-idp2"). + Build() + + myADIDP1 := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ad-idp1"). + WithResourceUID("my-ad-uid-idp1"). + Build() + myADIDP2 := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ad-idp2"). + WithResourceUID("my-ad-uid-idp2"). + Build() + + // FederationDomainIssuers + fakeIssuerURL := "https://www.fakeissuerurl.com" + fdIssuerWithoutIDP, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{}) + require.NoError(t, err) + fdIssuerWithDefaultOIDCIDP, err := NewFederationDomainIssuerWithDefaultIDP(fakeIssuerURL, &FederationDomainIdentityProvider{ + DisplayName: "my-default-oidc-idp", + UID: "my-default-oidc-uid-idp", + }) + require.NoError(t, err) + fdIssuerWithDefaultLDAPIDP, err := NewFederationDomainIssuerWithDefaultIDP(fakeIssuerURL, &FederationDomainIdentityProvider{ + DisplayName: "my-default-ldap-idp", + UID: "my-default-ldap-uid-idp", + }) + require.NoError(t, err) + fdIssuerWithOIDCIDP1, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + {DisplayName: "my-oidc-idp1", UID: "my-oidc-uid-idp1"}, + }) + require.NoError(t, err) + fdIssuerWithOIDCIDP2, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + {DisplayName: "my-oidc-idp1", UID: "my-oidc-uid-idp1"}, + {DisplayName: "my-oidc-idp2", UID: "my-oidc-uid-idp2"}, + }) + require.NoError(t, err) + + fdIssuerWithOIDCAndLDAPAndADIDPs, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + {DisplayName: "my-oidc-idp1", UID: "my-oidc-uid-idp1"}, + {DisplayName: "my-oidc-idp2", UID: "my-oidc-uid-idp2"}, + {DisplayName: "my-ldap-idp1", UID: "my-ldap-uid-idp1"}, + {DisplayName: "my-ldap-idp2", UID: "my-ldap-uid-idp2"}, + {DisplayName: "my-ad-idp1", UID: "my-ad-uid-idp1"}, + {DisplayName: "my-ad-idp2", UID: "my-ad-uid-idp2"}, + }) + require.NoError(t, err) + + fdIssuerWithLotsOfIDPs, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + {DisplayName: "my-oidc-idp1", UID: "my-oidc-uid-idp1"}, + {DisplayName: "my-oidc-idp2", UID: "my-oidc-uid-idp2"}, + {DisplayName: "my-ldap-idp1", UID: "my-ldap-uid-idp1"}, + {DisplayName: "my-ldap-idp2", UID: "my-ldap-uid-idp2"}, + {DisplayName: "my-ad-idp1", UID: "my-ad-uid-idp1"}, + {DisplayName: "my-oidc-idp3", UID: "my-oidc-uid-idp3"}, + {DisplayName: "my-oidc-idp4", UID: "my-oidc-uid-idp4"}, + {DisplayName: "my-ldap-idp3", UID: "my-ldap-uid-idp3"}, + {DisplayName: "my-ldap-idp4", UID: "my-ldap-uid-idp4"}, + {DisplayName: "my-ad-idp2", UID: "my-ad-uid-idp2"}, + {DisplayName: "my-ad-idp3", UID: "my-ad-uid-idp3"}, + }) + require.NoError(t, err) + + fdIssuerWithIDPwithLostUID, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + {DisplayName: "my-idp", UID: "you-cant-find-my-uid"}, + }) + require.NoError(t, err) + + // Resolved IdPs + myOIDCIDP1Resolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: "my-oidc-idp1", + Provider: myOIDCIDP1, + SessionProviderType: "oidc", + } + myOIDCIDP2Resolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: "my-oidc-idp2", + Provider: myOIDCIDP2, + SessionProviderType: "oidc", + } + myLDAPIDP1Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: "my-ldap-idp1", + Provider: myLDAPIDP1, + SessionProviderType: "ldap", + } + myLDAPIDP2Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: "my-ldap-idp2", + Provider: myLDAPIDP2, + SessionProviderType: "ldap", + } + myADIDP1Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: "my-ad-idp1", + Provider: myADIDP1, + SessionProviderType: "activedirectory", + } + myADIDP2Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: "my-ad-idp2", + Provider: myADIDP2, + SessionProviderType: "activedirectory", + } + + myDefaultOIDCIDPResolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: "my-default-oidc-idp", + Provider: myDefaultOIDCIDP, + SessionProviderType: "oidc", + } + myDefaultLDAPIDPResolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: "my-default-ldap-idp", + Provider: myDefaultLDAPIDP, + SessionProviderType: "ldap", + } + + testFindUpstreamIDPByDisplayName := []struct { + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + findIDPByDisplayName string + wantOIDCIDPByDisplayName *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider + wantLDAPIDPByDisplayName *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + wantError string + }{ + { + name: "FindUpstreamIDPByDisplayName will find an upstream IdP by display name with one IDP configured", + findIDPByDisplayName: "my-oidc-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithLDAP(myLDAPIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCIDP1, + wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, + }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP by display name if multiple IDPs configured of the same type", + findIDPByDisplayName: "my-oidc-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCIDP2, + wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, + }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP by display name if multiple IDPs configured of different types", + findIDPByDisplayName: "my-oidc-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, + }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type OIDC by display name", + findIDPByDisplayName: "my-oidc-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCIDP1, + wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, + }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type LDAP by display name", + findIDPByDisplayName: "my-ldap-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type AD (LDAP) by display name", + findIDPByDisplayName: "my-ad-idp1", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantLDAPIDPByDisplayName: myADIDP1Resolved, + }, + { + name: "FindUpstreamIDPByDisplayName will error if IDP by display name is not found - no such display name", + findIDPByDisplayName: "i-cant-find-my-idp", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantError: `identity provider not found: "i-cant-find-my-idp"`, + }, + { + name: "FindUpstreamIDPByDisplayName will error if IDP by display name is not found - display name was found, but IDP it points at does not exist", + findIDPByDisplayName: "my-idp", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithIDPwithLostUID, + wantError: `identity provider not available: "my-idp"`, + }, + } + + for _, tt := range testFindUpstreamIDPByDisplayName { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) + foundOIDCIDP, foundLDAPIDP, err := subject.FindUpstreamIDPByDisplayName(tt.findIDPByDisplayName) + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + } else { + require.NoError(t, err) + } + if tt.wantOIDCIDPByDisplayName != nil { + require.Equal(t, foundOIDCIDP, tt.wantOIDCIDPByDisplayName) + } + if tt.wantLDAPIDPByDisplayName != nil { + require.Equal(t, foundLDAPIDP, tt.wantLDAPIDPByDisplayName) + } + }) + } + + testFindDefaultIDP := []struct { + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + wantDefaultOIDCIDP *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider + wantDefaultLDAPIDP *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + wantError string + }{ + { + name: "FindDefaultIDP returns an OIDCIdentityProvider if there is an OIDCIdentityProvider defined as the default IDP", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myDefaultOIDCIDP). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithDefaultOIDCIDP, + wantDefaultOIDCIDP: myDefaultOIDCIDPResolved, + }, + { + name: "FindDefaultIDP returns an LDAPIdentityProvider if there is an LDAPIdentityProvider defined as the default IDP", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(myDefaultLDAPIDP). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, + wantDefaultLDAPIDP: myDefaultLDAPIDPResolved, + }, + { + name: "FindDefaultIDP returns an error if there is no default IDP to return", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(myDefaultLDAPIDP). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithoutIDP, + wantError: "identity provider not found: this federation domain does not have a default identity provider", + }, + { + name: "FindDefaultIDP returns an error if there are multiple IDPs configured", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithLDAP(myLDAPIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithLotsOfIDPs, + wantError: "identity provider not found: this federation domain does not have a default identity provider", + }, + { + name: "FindDefaultIDP returns an error if the wrapped lister does not contain the default IDP (not available)", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-default-ldap-idp"). + WithResourceUID("my-ldap-idp-resource-uid-does-not-match"). + Build()). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, + wantError: `identity provider not available: "my-default-ldap-idp"`, + }, + } + + for _, tt := range testFindDefaultIDP { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) + foundOIDCIDP, foundLDAPIDP, err := subject.FindDefaultIDP() + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + } else { + require.NoError(t, err) + } + if tt.wantDefaultOIDCIDP != nil { + require.Equal(t, foundOIDCIDP, tt.wantDefaultOIDCIDP) + } + if tt.wantDefaultLDAPIDP != nil { + require.Equal(t, foundLDAPIDP, tt.wantDefaultLDAPIDP) + } + }) + } + + testGetOIDCIdentityProviders := []struct { + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + wantIDPs []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider + }{ + { + name: "GetOIDCIdentityProviders will list all OIDCIdentityProviders", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + myOIDCIDP1Resolved, + myOIDCIDP2Resolved, + }, + }, + { + name: "GetLDAPIdentityProviders will return a list of LDAP IDPs if there are LDAPIdentityProviders configured but exclude LDAP IDPs that do not have matching UIDs", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName("my-oidc-idp-that-isnt-in-fd-issuer"). + WithResourceUID("my-oidc-idp-that-isnt-in-fd-issuer"). + Build()). + WithLDAP(myLDAPIDP1). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithLotsOfIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + myOIDCIDP1Resolved, + myOIDCIDP2Resolved, + }, + }, + { + name: "GetOIDCIdentityProviders will return nil of no OIDCIDentityProviders are found", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{}, + }, + } + + for _, tt := range testGetOIDCIdentityProviders { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) + idps := subject.GetOIDCIdentityProviders() + + require.Equal(t, idps, tt.wantIDPs) + }) + } + + testGetLDAPIdentityProviders := []struct { + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + wantIDPs []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + }{ + { + name: "GetLDAPIdentityProviders will list all LDAPIdentityProviders", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myLDAPIDP1Resolved, + myLDAPIDP2Resolved, + }, + }, + { + name: "GetLDAPIdentityProviders will return a list of LDAP IDPs if there are LDAPIdentityProviders configured but exclude LDAP IDPs that do not have matching UIDs", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ldap-idp-that-isnt-in-fd-issuer"). + WithResourceUID("my-ldap-idp-that-isnt-in-fd-issuer"). + Build()). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithLotsOfIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myLDAPIDP1Resolved, + }, + }, + { + name: "GetLDAPIdentityProviders will return an empty list of IDPs if no LDAPIdentityProviders are found", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithActiveDirectory(myADIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{}, + }, + } + for _, tt := range testGetLDAPIdentityProviders { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) + idps := subject.GetLDAPIdentityProviders() + + require.Equal(t, idps, tt.wantIDPs) + }) + } + + testGetActiveDirectoryIdentityProviders := []struct { + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + wantIDPs []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + }{ + { + name: "GetActiveDirectoryIdentityProviders will return a list of LDAP IDPs if there are ActiveDirectoryIdentityProviders configured", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithActiveDirectory(myADIDP1). + WithActiveDirectory(myADIDP2). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myADIDP1Resolved, + myADIDP2Resolved, + }, + }, + { + name: "GetActiveDirectoryIdentityProviders will return a list of LDAP IDPs if there are ActiveDirectoryIdentityProviders configured but exclude AD IDPs that do not have matching UIDs", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithActiveDirectory(myADIDP1). + WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName("my-ad-idp-that-isnt-in-fd-issuer"). + WithResourceUID("my-ad-idp-that-isnt-in-fd-issuer"). + Build()). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithLotsOfIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myADIDP1Resolved, + }, + }, + { + name: "GetActiveDirectoryIdentityProviders will return an empty list of LDAP IDPs if no ActiveDirectoryIdentityProviders are found", + wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{}, + }, + } + + for _, tt := range testGetActiveDirectoryIdentityProviders { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) + idps := subject.GetActiveDirectoryIdentityProviders() + + require.Equal(t, idps, tt.wantIDPs) + }) + } +} From b71e5964aa96b9421117e67ecab5e9a32328d10d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 12:40:13 -0700 Subject: [PATCH 17/81] fix token_handler_test.go --- .../endpoints/token/token_handler.go | 57 ++++++++++++------- .../endpoints/token/token_handler_test.go | 40 +++++++------ 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index 8f6512936..719d744e6 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -135,6 +135,8 @@ func upstreamOIDCRefresh( clientID string, ) error { s := session.Custom + groupsScopeGranted := slices.Contains(grantedScopes, oidcapi.ScopeGroups) + if s.OIDC == nil { return errorsx.WithStack(errMissingUpstreamSessionInternalError()) } @@ -186,15 +188,6 @@ func upstreamOIDCRefresh( } mergedClaims := validatedTokens.IDToken.Claims - oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) - if err != nil { - return err - } - oldTransformedGroups, err := getDownstreamGroupsFromPinnipedSession(session) - if err != nil { - return err - } - // To the extent possible, check that the user's basic identity hasn't changed. err = validateSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims, session) if err != nil { @@ -202,8 +195,7 @@ func upstreamOIDCRefresh( } var refreshedUntransformedGroups []string - groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups) - if groupsScope { + if groupsScopeGranted { // If possible, update the user's group memberships. The configured groups claim name (if there is one) may or // may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the // claim name. It could also be missing because the claim was originally found in the ID token during login, but @@ -231,9 +223,25 @@ func upstreamOIDCRefresh( if refreshedUntransformedGroups == nil { // If we could not get a new list of groups, then we still need the untransformed groups list to be able to // run the transformations again, so fetch the original untransformed groups list from the session. + // We should also run the transformations on the original groups even when the groups scope was not granted, + // because a transformation policy may want to reject the authentication based on the group memberships, even + // though the group memberships will not be shared with the client (in the code below) due to the groups scope + // not being granted. refreshedUntransformedGroups = s.UpstreamGroups } + oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) + if err != nil { + return err + } + var oldTransformedGroups []string + if groupsScopeGranted { + oldTransformedGroups, err = getDownstreamGroupsFromPinnipedSession(session) + if err != nil { + return err + } + } + transformationResult, err := transformRefreshedIdentity(ctx, p.Transforms, oldTransformedUsername, @@ -246,8 +254,11 @@ func upstreamOIDCRefresh( return err } - warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) - session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = refreshedUntransformedGroups + if groupsScopeGranted { + warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) + // Replace the old value with the new value. + session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = transformationResult.Groups + } // Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in // the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding @@ -334,19 +345,20 @@ func upstreamLDAPRefresh( grantedScopes []string, clientID string, ) error { + s := session.Custom + groupsScopeGranted := slices.Contains(grantedScopes, oidcapi.ScopeGroups) + oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) if err != nil { return err } - subject := session.Fosite.Claims.Subject var oldTransformedGroups []string - if slices.Contains(grantedScopes, oidcapi.ScopeGroups) { + if groupsScopeGranted { oldTransformedGroups, err = getDownstreamGroupsFromPinnipedSession(session) if err != nil { return err } } - s := session.Custom validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != "" validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != "" @@ -369,9 +381,13 @@ func upstreamLDAPRefresh( return errorsx.WithStack(errMissingUpstreamSessionInternalError()) } + plog.Debug("attempting upstream refresh request", + "providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID) + + oldUntransformedUsername := s.UpstreamUsername refreshedUntransformedGroups, err := p.Provider.PerformRefresh(ctx, upstreamprovider.RefreshAttributes{ - Username: s.UpstreamUsername, - Subject: subject, + Username: oldUntransformedUsername, + Subject: session.Fosite.Claims.Subject, DN: dn, Groups: s.UpstreamGroups, AdditionalAttributes: additionalAttributes, @@ -386,7 +402,7 @@ func upstreamLDAPRefresh( transformationResult, err := transformRefreshedIdentity(ctx, p.Transforms, oldTransformedUsername, - s.UpstreamUsername, + oldUntransformedUsername, // LDAP PerformRefresh validates that the username did not change, so this is also the refreshed upstream username refreshedUntransformedGroups, s.ProviderName, s.ProviderType, @@ -395,8 +411,7 @@ func upstreamLDAPRefresh( return err } - groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups) - if groupsScope { + if groupsScopeGranted { warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) // Replace the old value with the new value. session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = transformationResult.Groups diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 48656f284..764f2c289 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -1763,10 +1763,12 @@ func TestRefreshGrant(t *testing.T) { initialUpstreamOIDCRefreshTokenCustomSessionData := func() *psession.CustomSessionData { return &psession.CustomSessionData{ - Username: goodUsername, - ProviderName: oidcUpstreamName, - ProviderUID: oidcUpstreamResourceUID, - ProviderType: oidcUpstreamType, + Username: goodUsername, + UpstreamUsername: goodUsername, + UpstreamGroups: goodGroups, + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: oidcUpstreamInitialRefreshToken, UpstreamSubject: goodUpstreamSubject, @@ -1777,10 +1779,12 @@ func TestRefreshGrant(t *testing.T) { initialUpstreamOIDCAccessTokenCustomSessionData := func() *psession.CustomSessionData { return &psession.CustomSessionData{ - Username: goodUsername, - ProviderName: oidcUpstreamName, - ProviderUID: oidcUpstreamResourceUID, - ProviderType: oidcUpstreamType, + Username: goodUsername, + UpstreamUsername: goodUsername, + UpstreamGroups: goodGroups, + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, UpstreamSubject: goodUpstreamSubject, @@ -1917,20 +1921,24 @@ func TestRefreshGrant(t *testing.T) { } happyActiveDirectoryCustomSessionData := &psession.CustomSessionData{ - Username: goodUsername, - ProviderUID: activeDirectoryUpstreamResourceUID, - ProviderName: activeDirectoryUpstreamName, - ProviderType: activeDirectoryUpstreamType, + Username: goodUsername, + UpstreamUsername: goodUsername, + UpstreamGroups: goodGroups, + ProviderUID: activeDirectoryUpstreamResourceUID, + ProviderName: activeDirectoryUpstreamName, + ProviderType: activeDirectoryUpstreamType, ActiveDirectory: &psession.ActiveDirectorySessionData{ UserDN: activeDirectoryUpstreamDN, }, } happyLDAPCustomSessionData := &psession.CustomSessionData{ - Username: goodUsername, - ProviderUID: ldapUpstreamResourceUID, - ProviderName: ldapUpstreamName, - ProviderType: ldapUpstreamType, + Username: goodUsername, + UpstreamUsername: goodUsername, + UpstreamGroups: goodGroups, + ProviderUID: ldapUpstreamResourceUID, + ProviderName: ldapUpstreamName, + ProviderType: ldapUpstreamType, LDAP: &psession.LDAPSessionData{ UserDN: ldapUpstreamDN, }, From 048f05d39cc5686afad0f9bfdd5e7e2169e1f23f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 13:05:21 -0700 Subject: [PATCH 18/81] fix callback_handler_test.go Co-authored-by: Benjamin A. Petersen --- .../callback/callback_handler_test.go | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/internal/federationdomain/endpoints/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go index 15ec387bb..08ce3d3a8 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -90,28 +90,34 @@ var ( happyDownstreamRequestParamsForDynamicClient = happyDownstreamRequestParamsQueryForDynamicClient.Encode() happyDownstreamCustomSessionData = &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, - ProviderType: psession.ProviderTypeOIDC, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: happyUpstreamIDPResourceUID, + ProviderName: happyUpstreamIDPName, + ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: oidcUpstreamRefreshToken, UpstreamIssuer: oidcUpstreamIssuer, UpstreamSubject: oidcUpstreamSubject, }, } - happyDownstreamCustomSessionDataWithUsername = func(wantUsername string) *psession.CustomSessionData { + happyDownstreamCustomSessionDataWithUsernameAndGroups = func(wantUsername string, wantGroups []string) *psession.CustomSessionData { copyOfCustomSession := *happyDownstreamCustomSessionData copyOfOIDC := *(happyDownstreamCustomSessionData.OIDC) copyOfCustomSession.OIDC = ©OfOIDC copyOfCustomSession.Username = wantUsername + copyOfCustomSession.UpstreamUsername = wantUsername + copyOfCustomSession.UpstreamGroups = wantGroups return ©OfCustomSession } happyDownstreamAccessTokenCustomSessionData = &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, - ProviderType: psession.ProviderTypeOIDC, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: happyUpstreamIDPResourceUID, + ProviderName: happyUpstreamIDPName, + ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, UpstreamIssuer: oidcUpstreamIssuer, @@ -395,11 +401,13 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: &psession.CustomSessionData{ - Username: oidcUpstreamUsername, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, - ProviderType: psession.ProviderTypeOIDC, - Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."}, + Username: oidcUpstreamUsername, + UpstreamUsername: oidcUpstreamUsername, + UpstreamGroups: oidcUpstreamGroupMembership, + ProviderUID: happyUpstreamIDPResourceUID, + ProviderName: happyUpstreamIDPName, + ProviderType: psession.ProviderTypeOIDC, + Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."}, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, UpstreamIssuer: oidcUpstreamIssuer, @@ -431,7 +439,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername(oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, nil), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -457,7 +465,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe@whitehouse.gov"), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe@whitehouse.gov", oidcUpstreamGroupMembership), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -485,7 +493,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe@whitehouse.gov"), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe@whitehouse.gov", oidcUpstreamGroupMembership), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -514,7 +522,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe"), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe", oidcUpstreamGroupMembership), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -645,7 +653,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername(oidcUpstreamSubject), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamSubject, oidcUpstreamGroupMembership), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -671,7 +679,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, []string{"notAnArrayGroup1 notAnArrayGroup2"}), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -697,7 +705,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, []string{"group1", "group2"}), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -1252,7 +1260,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, nil), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, From 98ee9f09799b37ea7844dfd7b76c6d835670cb68 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 14:06:46 -0700 Subject: [PATCH 19/81] escape semicolons in variable values in integration-test-env-goland.sh Co-authored-by: Benjamin A. Petersen --- hack/integration-test-env-goland.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hack/integration-test-env-goland.sh b/hack/integration-test-env-goland.sh index db2c0bc45..c4991627d 100755 --- a/hack/integration-test-env-goland.sh +++ b/hack/integration-test-env-goland.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +# Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -17,7 +17,9 @@ echo -n "PINNIPED_TEST_GOLAND_RUNNER=true;" printenv | grep PINNIPED_TEST_ | sed 's/=.*//g' | grep -v CLUSTER_CAPABILITY_YAML | while read -r var ; do echo -n "${var}=" - echo -n "${!var}" | tr -d '\n' + # Goland will treat semicolons as key/value pair separators. + # Within a value, a semicolon needs to be escaped with a backslash for Goland. + echo -n "${!var}" | sed 's/;/\\;/g' | tr -d '\n' echo -n ";" done From 0f23931fe421df3fc03a25fcd8fc34baffd40e83 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 15:26:24 -0700 Subject: [PATCH 20/81] Fix some tests in supervisor_login_test.go --- .../endpoints/token/token_handler.go | 21 +++++----- test/integration/supervisor_login_test.go | 40 ++++++++++++------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index 719d744e6..c0f22ea53 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -116,10 +116,8 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, switch customSessionData.ProviderType { case psession.ProviderTypeOIDC: - return upstreamOIDCRefresh(ctx, session, idpLister, grantedScopes, clientID) - case psession.ProviderTypeLDAP: - return upstreamLDAPRefresh(ctx, idpLister, session, grantedScopes, clientID) - case psession.ProviderTypeActiveDirectory: + return upstreamOIDCRefresh(ctx, idpLister, session, grantedScopes, clientID) + case psession.ProviderTypeLDAP, psession.ProviderTypeActiveDirectory: return upstreamLDAPRefresh(ctx, idpLister, session, grantedScopes, clientID) default: return errorsx.WithStack(errMissingUpstreamSessionInternalError()) @@ -129,8 +127,8 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, //nolint:funlen func upstreamOIDCRefresh( ctx context.Context, - session *psession.PinnipedSession, idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, + session *psession.PinnipedSession, grantedScopes []string, clientID string, ) error { @@ -215,10 +213,12 @@ func upstreamOIDCRefresh( // but if it is, verify that the transformed version of it hasn't changed. refreshedUntransformedUsername, hasRefreshedUntransformedUsername := getString(mergedClaims, p.Provider.GetUsernameClaim()) + oldUntransformedUsername := s.UpstreamUsername + oldUntransformedGroups := s.UpstreamGroups if !hasRefreshedUntransformedUsername { // If we could not get a new username, then we still need the untransformed username to be able to // run the transformations again, so fetch the original untransformed username from the session. - refreshedUntransformedUsername = s.UpstreamUsername + refreshedUntransformedUsername = oldUntransformedUsername } if refreshedUntransformedGroups == nil { // If we could not get a new list of groups, then we still need the untransformed groups list to be able to @@ -227,7 +227,7 @@ func upstreamOIDCRefresh( // because a transformation policy may want to reject the authentication based on the group memberships, even // though the group memberships will not be shared with the client (in the code below) due to the groups scope // not being granted. - refreshedUntransformedGroups = s.UpstreamGroups + refreshedUntransformedGroups = oldUntransformedGroups } oldTransformedUsername, err := getDownstreamUsernameFromPinnipedSession(session) @@ -256,7 +256,7 @@ func upstreamOIDCRefresh( if groupsScopeGranted { warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) - // Replace the old value with the new value. + // Replace the old value for the downstream groups in the user's session with the new value. session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = transformationResult.Groups } @@ -385,11 +385,12 @@ func upstreamLDAPRefresh( "providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID) oldUntransformedUsername := s.UpstreamUsername + oldUntransformedGroups := s.UpstreamGroups refreshedUntransformedGroups, err := p.Provider.PerformRefresh(ctx, upstreamprovider.RefreshAttributes{ Username: oldUntransformedUsername, Subject: session.Fosite.Claims.Subject, DN: dn, - Groups: s.UpstreamGroups, + Groups: oldUntransformedGroups, AdditionalAttributes: additionalAttributes, GrantedScopes: grantedScopes, }) @@ -413,7 +414,7 @@ func upstreamLDAPRefresh( if groupsScopeGranted { warnIfGroupsChanged(ctx, oldTransformedGroups, transformationResult.Groups, transformationResult.Username, clientID) - // Replace the old value with the new value. + // Replace the old value for the downstream groups in the user's session with the new value. session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = transformationResult.Groups } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index d424014c0..8567e1940 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -297,11 +297,13 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { - // even if we update this group to the wrong thing, we expect that it will return to the correct - // value after we refresh. + // Even if we update this group to the some names that did not come from the OIDC server, + // we expect that it will return to the real groups from the OIDC server after we refresh. // However if there are no expected groups then they will not update, so we should skip this. if len(env.SupervisorUpstreamOIDC.ExpectedGroups) > 0 { - sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"} + 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.SupervisorUpstreamOIDC.ExpectedGroups }, @@ -450,9 +452,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { ) }, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { - // even if we update this group to the wrong thing, we expect that it will return to the correct - // value after we refresh. - sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"} + // 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) { @@ -656,11 +660,16 @@ func TestSupervisorLogin_Browser(t *testing.T) { ) }, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { - // update the list of groups to the wrong thing and see that they do not get updated because - // skip group refresh is set - wrongGroups := []string{"some-wrong-group", "some-other-group"} - sessionData.Fosite.Claims.Extra["groups"] = wrongGroups - return wrongGroups + // Update the list of groups to some groups that would not come from the real LDAP queries, + // and see that these become the user's new groups after refresh, because LDAP skip group refresh is set. + // Since we are skipping the LDAP group query during refresh, the refresh should use what the session + // says is the original list of untransformed groups from the initial login, and then perform the + // transformations on them again. However, since there are no transformations configured, they will not + // be changed by any transformations in this case. + initialGroupMembership := []string{"some-wrong-group", "some-other-group"} // these groups are not in LDAP server + sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session + sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session + return initialGroupMembership // these are the expected groups after the refresh is performed }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -709,9 +718,12 @@ func TestSupervisorLogin_Browser(t *testing.T) { ) }, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { - // even if we update this group to the wrong thing, we expect that it will return to the correct - // value (no groups) after we refresh. - sessionData.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"} + // 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, + // which in this case is no groups since this test uses a group search base which results in no groups. + 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 []string{} }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { From 2c4927debe6ed21ff5d246ea560bcc40d4c51928 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 15:43:50 -0700 Subject: [PATCH 21/81] update unit test that fails on slow CI workers --- internal/celtransformer/celformer_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 2bcdc96df..0adf9fa73 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -6,6 +6,7 @@ package celtransformer import ( "context" "fmt" + "runtime" "sync" "testing" "time" @@ -797,7 +798,7 @@ func TestTransformer(t *testing.T) { func TestTypicalPerformanceAndThreadSafety(t *testing.T) { t.Parallel() - transformer, err := NewCELTransformer(100 * time.Millisecond) + transformer, err := NewCELTransformer(time.Second) // CI workers can be slow, so allow slow transforms require.NoError(t, err) pipeline := idtransform.NewTransformationPipeline() @@ -845,7 +846,8 @@ func TestTypicalPerformanceAndThreadSafety(t *testing.T) { // exact same result, since they are all using the same inputs. This assumes that the unit tests are run using // the race detector. var wg sync.WaitGroup - numGoroutines := 10 + numGoroutines := runtime.NumCPU() / 2 + t.Logf("Running tight loops in %d simultaneous goroutines", numGoroutines) for i := 0; i < numGoroutines; i++ { wg.Add(1) // increment WaitGroup counter for each goroutine go func() { From 514f9964c139001388029a0d5fe7abb34196f187 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 26 Jun 2023 15:55:28 -0700 Subject: [PATCH 22/81] update 1.27 codegen for multiple IDPs --- generated/1.27/README.adoc | 152 +++++++++- .../config/v1alpha1/types_federationdomain.go | 181 +++++++++++- .../config/v1alpha1/zz_generated.deepcopy.go | 140 ++++++++++ ...rvisor.pinniped.dev_federationdomains.yaml | 261 +++++++++++++++++- 4 files changed, 715 insertions(+), 19 deletions(-) diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 41ef8ab75..463f3eb91 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -650,6 +650,25 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -687,7 +706,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -712,7 +734,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) +==== FederationDomainStatusCondition (string) @@ -744,8 +766,108 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -779,7 +901,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -804,9 +926,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -831,7 +953,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -843,7 +965,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1161,7 +1283,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1329,7 +1451,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1494,7 +1616,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 27de4401c..2048b4e77 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -42,6 +42,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +206,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..241b1aa96 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -152,6 +177,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in 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 71f7370d1..396d25511 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the From e4f43683d40728ba064329f29bdf325e464b3d0c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 27 Jun 2023 09:37:43 -0700 Subject: [PATCH 23/81] fix more integration tests for multiple IDPs --- test/integration/e2e_test.go | 85 +++++++++++--------- test/integration/kube_api_discovery_test.go | 2 +- test/integration/supervisor_login_test.go | 7 ++ test/integration/supervisor_storage_test.go | 2 +- test/integration/supervisor_warnings_test.go | 25 +++--- 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 7b43b6929..98df33dee 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -140,7 +140,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }) // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -191,7 +191,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // If the username and groups scope are not requested by the CLI, then the CLI still gets them, to allow for @@ -221,7 +222,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }) // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -276,8 +277,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted // scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will // assert that the expected username and groups claims/values are in the downstream ID token. - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, - pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) }) t.Run("with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) { @@ -305,7 +306,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }) // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -388,7 +389,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) t.Run("access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) { @@ -424,7 +426,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { } // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -524,7 +526,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) t.Run("with Supervisor OIDC upstream IDP and CLI password flow without web browser", func(t *testing.T) { @@ -549,7 +552,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }) // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -607,7 +610,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) t.Run("with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it", func(t *testing.T) { @@ -705,7 +709,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -743,7 +747,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // If the username and groups scope are not requested by the CLI, then the CLI still gets them, to allow for @@ -759,7 +764,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -801,8 +806,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted // scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will // assert that the expected username and groups claims/values are in the downstream ID token. - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, - pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) }) // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands @@ -818,7 +823,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -868,7 +873,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands @@ -884,7 +890,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames - setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -922,7 +928,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands @@ -938,7 +945,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames - setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -988,7 +995,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. @@ -1006,7 +1014,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1038,7 +1046,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. @@ -1056,7 +1065,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames - setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1088,7 +1097,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow. @@ -1106,7 +1116,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1144,7 +1154,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) } @@ -1241,7 +1252,7 @@ func waitForKubectlOutput(t *testing.T, kubectlOutputChan chan string) string { return kubectlOutput } -func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) { +func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) *idpv1alpha1.LDAPIdentityProvider { // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. testlib.CreateTestClusterRoleBinding(t, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: username}, @@ -1263,7 +1274,7 @@ func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib ) // Create upstream LDAP provider and wait for it to become ready. - testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + return testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ Host: env.SupervisorUpstreamLDAP.Host, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), @@ -1289,7 +1300,7 @@ func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib }, idpv1alpha1.LDAPPhaseReady) } -func setupClusterForEndToEndActiveDirectoryTest(t *testing.T, username string, env *testlib.TestEnv) { +func setupClusterForEndToEndActiveDirectoryTest(t *testing.T, username string, env *testlib.TestEnv) *idpv1alpha1.ActiveDirectoryIdentityProvider { // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. testlib.CreateTestClusterRoleBinding(t, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: username}, @@ -1311,7 +1322,7 @@ func setupClusterForEndToEndActiveDirectoryTest(t *testing.T, username string, e ) // Create upstream LDAP provider and wait for it to become ready. - testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + return testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ Host: env.SupervisorUpstreamActiveDirectory.Host, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), @@ -1369,6 +1380,7 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain( t *testing.T, env *testlib.TestEnv, downstream *configv1alpha1.FederationDomain, + upstreamProviderName string, kubeconfigPath string, sessionCachePath string, pinnipedExe string, @@ -1392,10 +1404,11 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain( sort.Strings(downstreamScopes) token := cache.GetToken(oidcclient.SessionCacheKey{ - Issuer: downstream.Spec.Issuer, - ClientID: "pinniped-cli", - Scopes: downstreamScopes, - RedirectURI: "http://localhost:0/callback", + Issuer: downstream.Spec.Issuer, + ClientID: "pinniped-cli", + Scopes: downstreamScopes, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: upstreamProviderName, }) require.NotNil(t, token) diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 38aabd138..2884c7153 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -441,7 +441,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr // over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all // Pinniped API resources. Without this, the test could accidentally skip parts of the tree if the // format has changed. - require.Equal(t, 230, foundFieldNames, + require.Equal(t, 254, foundFieldNames, "Expected to find all known fields of all Pinniped API resources. "+ "You may will need to update this expectation if you added new fields to the API types.", ) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 8567e1940..393d67a17 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -2539,7 +2539,14 @@ func makeAuthorizationRequestAndRequireSecurityHeaders(ctx context.Context, t *t require.NoError(t, err) authorizeResp, err := httpClient.Do(authorizeRequest) require.NoError(t, err) + body, err := io.ReadAll(authorizeResp.Body) + require.NoError(t, err) require.NoError(t, authorizeResp.Body.Close()) + if authorizeResp.StatusCode >= 400 { + // The request should not have failed, so print the response for debugging purposes. + t.Logf("makeAuthorizationRequestAndRequireSecurityHeaders authorization response: %#v", authorizeResp) + t.Logf("makeAuthorizationRequestAndRequireSecurityHeaders authorization response body: %q", body) + } expectSecurityHeaders(t, authorizeResp, false) } diff --git a/test/integration/supervisor_storage_test.go b/test/integration/supervisor_storage_test.go index d56cc6be1..458d5209c 100644 --- a/test/integration/supervisor_storage_test.go +++ b/test/integration/supervisor_storage_test.go @@ -91,7 +91,7 @@ func TestAuthorizeCodeStorage(t *testing.T) { // Note that CreateAuthorizeCodeSession() sets Active to true and also sets the Version before storing the session, // so expect those here. session.Active = true - session.Version = "4" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant + session.Version = "5" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant expectedSessionStorageJSON, err := json.Marshal(session) require.NoError(t, err) require.JSONEq(t, string(expectedSessionStorageJSON), string(initialSecret.Data["pinniped-storage-data"])) diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index 5c93e687b..a025c8887 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -106,7 +106,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue - setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" @@ -174,10 +174,11 @@ func TestSupervisorWarnings_Browser(t *testing.T) { downstreamScopes := []string{"offline_access", "openid", "pinniped:request-audience", "groups"} sort.Strings(downstreamScopes) sessionCacheKey := oidcclient.SessionCacheKey{ - Issuer: downstream.Spec.Issuer, - ClientID: "pinniped-cli", - Scopes: downstreamScopes, - RedirectURI: "http://localhost:0/callback", + Issuer: downstream.Spec.Issuer, + ClientID: "pinniped-cli", + Scopes: downstreamScopes, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: createdProvider.Name, } // use it to get the cache entry token := cache.GetToken(sessionCacheKey) @@ -195,7 +196,8 @@ func TestSupervisorWarnings_Browser(t *testing.T) { // change the groups to simulate them changing in the IDP. pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession) require.True(t, ok, "should have been able to cast session data to PinnipedSession") - pinnipedSession.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"} + pinnipedSession.Custom.UpstreamGroups = []string{"some-wrong-group", "some-other-group"} // update upstream groups + pinnipedSession.Fosite.Claims.Extra["groups"] = []string{"some-wrong-group", "some-other-group"} // update downstream groups require.NoError(t, oauthStore.DeleteRefreshTokenSession(ctx, refreshTokenSignature)) require.NoError(t, oauthStore.CreateRefreshTokenSession(ctx, refreshTokenSignature, storedRefreshSession)) @@ -372,7 +374,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { }) // Create upstream OIDC provider and wait for it to become ready. - testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + createdProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), @@ -482,10 +484,11 @@ func TestSupervisorWarnings_Browser(t *testing.T) { downstreamScopes := []string{"offline_access", "openid", "pinniped:request-audience", "groups"} sort.Strings(downstreamScopes) sessionCacheKey := oidcclient.SessionCacheKey{ - Issuer: downstream.Spec.Issuer, - ClientID: "pinniped-cli", - Scopes: downstreamScopes, - RedirectURI: "http://localhost:0/callback", + Issuer: downstream.Spec.Issuer, + ClientID: "pinniped-cli", + Scopes: downstreamScopes, + RedirectURI: "http://localhost:0/callback", + UpstreamProviderName: createdProvider.Name, } // use it to get the cache entry token := cache.GetToken(sessionCacheKey) From 022fdb9cfd8523fea3f5b99b2844c86f2af293b3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 28 Jun 2023 10:01:23 -0700 Subject: [PATCH 24/81] Update a test assertion to make failure easier to understand --- internal/supervisor/server/server.go | 2 +- test/integration/supervisor_login_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 5018038f2..153869b01 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -441,7 +441,7 @@ func runSupervisor(ctx context.Context, podInfo *downward.PodInfo, cfg *supervis dynamicUpstreamIDPProvider := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() secretCache := secret.Cache{} - // OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux. + // OIDC endpoints will be served by the endpoints manager, and any non-OIDC paths will fallback to the healthMux. oidProvidersManager := endpointsmanager.NewManager( healthMux, dynamicJWKSProvider, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 393d67a17..3bfd780d3 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -2547,6 +2547,8 @@ func makeAuthorizationRequestAndRequireSecurityHeaders(ctx context.Context, t *t t.Logf("makeAuthorizationRequestAndRequireSecurityHeaders authorization response: %#v", authorizeResp) t.Logf("makeAuthorizationRequestAndRequireSecurityHeaders authorization response body: %q", body) } + require.Less(t, authorizeResp.StatusCode, 400, + "expected a successful authorize response, but got a response status indicating an error (see log above)") expectSecurityHeaders(t, authorizeResp, false) } From 0b408f4fc0770f8a8a366db4bc2c72282126af52 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 30 Jun 2023 13:43:40 -0700 Subject: [PATCH 25/81] Change FederationDomain.Status to use Phase and Conditions --- .../v1alpha1/types_federationdomain.go.tmpl | 40 +- .../config/v1alpha1/types_oidcclient.go.tmpl | 12 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.21/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.22/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.23/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.24/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.25/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.26/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- generated/1.27/README.adoc | 5 +- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- ...rvisor.pinniped.dev_federationdomains.yaml | 95 ++- .../config/v1alpha1/types_federationdomain.go | 40 +- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 9 +- .../conditionsutil/conditions_util.go | 4 +- .../federation_domain_watcher.go | 322 ++++++---- .../federation_domain_watcher_test.go | 584 +++++++----------- .../oidcclientwatcher/oidc_client_watcher.go | 7 +- test/integration/e2e_test.go | 2 +- test/integration/kube_api_discovery_test.go | 2 +- test/integration/supervisor_discovery_test.go | 54 +- test/integration/supervisor_login_test.go | 29 +- test/integration/supervisor_warnings_test.go | 2 +- test/testlib/client.go | 31 +- 51 files changed, 1431 insertions(+), 941 deletions(-) diff --git a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl index 2048b4e77..0290c618b 100644 --- a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl index 48f5de378..61106fdba 100644 --- a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index 396d25511..cb7e1a519 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 9a5fcd685..4710a9b7f 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -728,9 +728,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go index 2048b4e77..0290c618b 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 396d25511..cb7e1a519 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 58e9b9353..178c5bfcd 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -728,9 +728,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go index 2048b4e77..0290c618b 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 396d25511..cb7e1a519 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index dfd2ef936..7044b5f46 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -728,9 +728,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go index 2048b4e77..0290c618b 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 396d25511..cb7e1a519 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index c68bd1371..5d92b7054 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -728,9 +728,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== 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 2048b4e77..0290c618b 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return 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 396d25511..cb7e1a519 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 30e9a6860..eab5d0d14 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -726,9 +726,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== 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 2048b4e77..0290c618b 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return 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 396d25511..cb7e1a519 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index a8aa6a916..22bbcf2ac 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -726,9 +726,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== 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 2048b4e77..0290c618b 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return 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 396d25511..cb7e1a519 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 463f3eb91..1f0e01aeb 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -726,9 +726,8 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== 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 2048b4e77..0290c618b 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 241b1aa96..0800faf56 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -143,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return 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 396d25511..cb7e1a519 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,8 +21,8 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status - name: Status + - jsonPath: .status.phase + name: Phase type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -348,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -402,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go index 2048b4e77..0290c618b 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -263,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 539158fac..e1af0fd0a 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -142,9 +142,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return diff --git a/internal/controller/conditionsutil/conditions_util.go b/internal/controller/conditionsutil/conditions_util.go index 2521a3a6c..0bbe3603a 100644 --- a/internal/controller/conditionsutil/conditions_util.go +++ b/internal/controller/conditionsutil/conditions_util.go @@ -67,11 +67,11 @@ func mergeIDPCondition(existing *[]v1.Condition, new *v1.Condition) bool { } // MergeConfigConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. -func MergeConfigConditions(conditions []*v1.Condition, observedGeneration int64, conditionsToUpdate *[]v1.Condition, log plog.MinLogger) bool { +func MergeConfigConditions(conditions []*v1.Condition, observedGeneration int64, conditionsToUpdate *[]v1.Condition, log plog.MinLogger, now v1.Time) bool { hadErrorCondition := false for i := range conditions { cond := conditions[i].DeepCopy() - cond.LastTransitionTime = v1.Now() + cond.LastTransitionTime = now cond.ObservedGeneration = observedGeneration if mergeConfigCondition(conditionsToUpdate, cond) { log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 3923b498c..c18b9fb13 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -10,12 +10,11 @@ import ( "strings" "time" + "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/client-go/util/retry" - "k8s.io/klog/v2" "k8s.io/utils/clock" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -24,12 +23,29 @@ import ( idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" "go.pinniped.dev/internal/celtransformer" pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/plog" ) +const ( + typeReady = "Ready" + typeIssuerURLValid = "IssuerURLValid" + typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" + typeIssuerIsUnique = "IssuerIsUnique" + + reasonSuccess = "Success" + reasonNotReady = "NotReady" + reasonUnableToValidate = "UnableToValidate" + reasonInvalidIssuerURL = "InvalidIssuerURL" + reasonDuplicateIssuer = "DuplicateIssuer" + reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" + + celTransformerMaxExpressionRuntime = 5 * time.Second +) + // FederationDomainsSetter can be notified of all known valid providers with its SetIssuer function. // If there are no longer any valid issuers, then it can be called with no arguments. // Implementations of this type should be thread-safe to support calls from multiple goroutines. @@ -109,75 +125,14 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro return err } - // Make a map of issuer strings -> count of how many times we saw that issuer string. - // This will help us complain when there are duplicate issuer strings. - // Also make a helper function for forming keys into this map. - issuerCounts := make(map[string]int) - issuerURLToIssuerKey := func(issuerURL *url.URL) string { - return fmt.Sprintf("%s://%s%s", issuerURL.Scheme, strings.ToLower(issuerURL.Host), issuerURL.Path) - } - - // Make a map of issuer hostnames -> set of unique secret names. This will help us complain when - // multiple FederationDomains have the same issuer hostname (excluding port) but specify - // different TLS serving Secrets. Doesn't make sense to have the one address use more than one - // TLS cert. Ignore ports because SNI information on the incoming requests is not going to include - // port numbers. Also make a helper function for forming keys into this map. - uniqueSecretNamesPerIssuerAddress := make(map[string]map[string]bool) - issuerURLToHostnameKey := lowercaseHostWithoutPort - - for _, federationDomain := range federationDomains { - issuerURL, err := url.Parse(federationDomain.Spec.Issuer) - if err != nil { - continue // Skip url parse errors because they will be validated again below. - } - - issuerCounts[issuerURLToIssuerKey(issuerURL)]++ - - setOfSecretNames := uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)] - if setOfSecretNames == nil { - setOfSecretNames = make(map[string]bool) - uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)] = setOfSecretNames - } - if federationDomain.Spec.TLS != nil { - setOfSecretNames[federationDomain.Spec.TLS.SecretName] = true - } - } - var errs []error - federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) + crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) + for _, federationDomain := range federationDomains { - issuerURL, urlParseErr := url.Parse(federationDomain.Spec.Issuer) - - // Skip url parse errors because they will be validated below. - if urlParseErr == nil { - if issuerCount := issuerCounts[issuerURLToIssuerKey(issuerURL)]; issuerCount > 1 { - if err := c.updateStatus( - ctx.Context, - federationDomain.Namespace, - federationDomain.Name, - configv1alpha1.DuplicateFederationDomainStatusCondition, - "Duplicate issuer: "+federationDomain.Spec.Issuer, - ); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) - } - continue - } - } + conditions := make([]*configv1alpha1.Condition, 0, 4) - // Skip url parse errors because they will be validated below. - if urlParseErr == nil && len(uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)]) > 1 { - if err := c.updateStatus( - ctx.Context, - federationDomain.Namespace, - federationDomain.Name, - configv1alpha1.SameIssuerHostMustUseSameSecretFederationDomainStatusCondition, - "Issuers with the same DNS hostname (address not including port) must use the same secretName: "+issuerURLToHostnameKey(issuerURL), - ); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) - } - continue - } + conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) // TODO: Move all this identity provider stuff into helper functions. This is just a sketch of how the code would // work in the sense that this is not doing error handling, is not validating everything that it should, and @@ -232,7 +187,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } // If there is an explicit list of IDPs on the FederationDomain, then process the list. - celTransformer, _ := celtransformer.NewCELTransformer(time.Second) // TODO: what is a good duration limit here? + celTransformer, _ := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? // TODO: handle err for _, idp := range federationDomain.Spec.IdentityProviders { var idpResourceUID types.UID @@ -375,7 +330,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } if !stringSlicesEqual(e.Expects.Groups, result.Groups) { // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? - // TODO: What happens if the user did not write any group expectation? Treat it like expecting any empty list of groups? + // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? // TODO: handle this failed example plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", "federationDomain", federationDomain.Name, @@ -402,7 +357,6 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // Now that we have the list of IDPs for this FederationDomain, create the issuer. var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer - err = nil if defaultFederationDomainIdentityProvider != nil { // This is the constructor for the backwards compatibility mode. federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) @@ -411,31 +365,32 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) } if err != nil { - // Note that the FederationDomainIssuer constructors validate the Issuer URL. - if err := c.updateStatus( - ctx.Context, - federationDomain.Namespace, - federationDomain.Name, - configv1alpha1.InvalidFederationDomainStatusCondition, - "Invalid: "+err.Error(), - ); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) - } - continue + // Note that the FederationDomainIssuer constructors only validate the Issuer URL, + // so these are always issuer URL validation errors. + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonInvalidIssuerURL, + Message: err.Error(), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "spec.issuer is a valid URL", + }) } - if err := c.updateStatus( - ctx.Context, - federationDomain.Namespace, - federationDomain.Name, - configv1alpha1.SuccessFederationDomainStatusCondition, - "Provider successfully created", - ); err != nil { + if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { errs = append(errs, fmt.Errorf("could not update status: %w", err)) continue } - federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) + if !hadErrorCondition(conditions) { + // Successfully validated the FederationDomain, so allow it to be loaded. + federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) + } } c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) @@ -443,49 +398,168 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro return errors.NewAggregate(errs) } -func stringSlicesEqual(a []string, b []string) bool { - if len(a) != len(b) { - return false +func (c *federationDomainWatcherController) updateStatus( + ctx context.Context, + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) error { + updated := federationDomain.DeepCopy() + + if hadErrorCondition(conditions) { + updated.Status.Phase = configv1alpha1.FederationDomainPhaseError + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeReady, + Status: configv1alpha1.ConditionFalse, + Reason: reasonNotReady, + Message: "the FederationDomain is not ready: see other conditions for details", + }) + } else { + updated.Status.Phase = configv1alpha1.FederationDomainPhaseReady + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeReady, + Status: configv1alpha1.ConditionTrue, + Reason: 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), + }) } - for i, itemFromA := range a { - if b[i] != itemFromA { - return false - } + + _ = conditionsutil.MergeConfigConditions(conditions, + federationDomain.Generation, &updated.Status.Conditions, plog.New(), metav1.NewTime(c.clock.Now())) + + if equality.Semantic.DeepEqual(federationDomain, updated) { + return nil } - return true + + _, err := c.client. + ConfigV1alpha1(). + FederationDomains(federationDomain.Namespace). + UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + return err } -func (c *federationDomainWatcherController) updateStatus( - ctx context.Context, - namespace, name string, - status configv1alpha1.FederationDomainStatusCondition, - message string, -) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - federationDomain, err := c.client.ConfigV1alpha1().FederationDomains(namespace).Get(ctx, name, metav1.GetOptions{}) +type crossFederationDomainConfigValidator struct { + issuerCounts map[string]int + uniqueSecretNamesPerIssuerAddress map[string]map[string]bool +} + +func issuerURLToHostnameKey(issuerURL *url.URL) string { + return lowercaseHostWithoutPort(issuerURL) +} + +func issuerURLToIssuerKey(issuerURL *url.URL) string { + return fmt.Sprintf("%s://%s%s", issuerURL.Scheme, strings.ToLower(issuerURL.Host), issuerURL.Path) +} + +func (v *crossFederationDomainConfigValidator) Validate(federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + issuerURL, urlParseErr := url.Parse(federationDomain.Spec.Issuer) + + if urlParseErr != nil { + // Don't write a condition about the issuer URL being invalid because that is added elsewhere in the controller. + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerIsUnique, + Status: configv1alpha1.ConditionUnknown, + Reason: reasonUnableToValidate, + Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", + }) + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeOneTLSSecretPerIssuerHostname, + Status: configv1alpha1.ConditionUnknown, + Reason: 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 + } + + if issuerCount := v.issuerCounts[issuerURLToIssuerKey(issuerURL)]; issuerCount > 1 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerIsUnique, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDuplicateIssuer, + Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerIsUnique, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "spec.issuer is unique among all FederationDomains", + }) + } + + if len(v.uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)]) > 1 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeOneTLSSecretPerIssuerHostname, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDifferentSecretRefsFound, + Message: "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found", + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeOneTLSSecretPerIssuerHostname, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", + }) + } + + return conditions +} + +func newCrossFederationDomainConfigValidator(federationDomains []*configv1alpha1.FederationDomain) *crossFederationDomainConfigValidator { + // Make a map of issuer strings -> count of how many times we saw that issuer string. + // This will help us complain when there are duplicate issuer strings. + // Also make a helper function for forming keys into this map. + issuerCounts := make(map[string]int) + + // Make a map of issuer hostnames -> set of unique secret names. This will help us complain when + // multiple FederationDomains have the same issuer hostname (excluding port) but specify + // different TLS serving Secrets. Doesn't make sense to have the one address use more than one + // TLS cert. Ignore ports because SNI information on the incoming requests is not going to include + // port numbers. Also make a helper function for forming keys into this map. + uniqueSecretNamesPerIssuerAddress := make(map[string]map[string]bool) + + for _, federationDomain := range federationDomains { + issuerURL, err := url.Parse(federationDomain.Spec.Issuer) if err != nil { - return fmt.Errorf("get failed: %w", err) + continue // Skip url parse errors because they will be handled in the Validate function. } - if federationDomain.Status.Status == status && federationDomain.Status.Message == message { - return nil + issuerCounts[issuerURLToIssuerKey(issuerURL)]++ + + setOfSecretNames := uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)] + if setOfSecretNames == nil { + setOfSecretNames = make(map[string]bool) + uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)] = setOfSecretNames + } + if federationDomain.Spec.TLS != nil { + setOfSecretNames[federationDomain.Spec.TLS.SecretName] = true } + } - plog.Debug( - "attempting status update", - "federationdomain", - klog.KRef(namespace, name), - "status", - status, - "message", - message, - ) - federationDomain.Status.Status = status - federationDomain.Status.Message = message - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(c.clock.Now())) - _, err = c.client.ConfigV1alpha1().FederationDomains(namespace).UpdateStatus(ctx, federationDomain, metav1.UpdateOptions{}) - return err - }) + return &crossFederationDomainConfigValidator{ + issuerCounts: issuerCounts, + uniqueSecretNamesPerIssuerAddress: uniqueSecretNamesPerIssuerAddress, + } } -func timePtr(t metav1.Time) *metav1.Time { return &t } +func hadErrorCondition(conditions []*configv1alpha1.Condition) bool { + for _, c := range conditions { + if c.Status != configv1alpha1.ConditionTrue { + return true + } + } + return false +} + +func stringSlicesEqual(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for i, itemFromA := range a { + if b[i] != itemFromA { + return false + } + } + return true +} diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 0e75cdd34..f274424a2 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -16,7 +16,6 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/stretchr/testify/require" - k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -28,7 +27,6 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" - "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/testutil" ) @@ -113,8 +111,21 @@ func TestSync(t *testing.T) { var cancelContextCancelFunc context.CancelFunc var syncContext *controllerlib.Context var frozenNow time.Time + var frozenMetav1Now metav1.Time var federationDomainsSetter *fakeFederationDomainsSetter var federationDomainGVR schema.GroupVersionResource + var allHappyConditions func(issuer string, time metav1.Time, observedGeneration int64) []v1alpha1.Condition + var happyReadyCondition func(issuer string, time metav1.Time, observedGeneration int64) v1alpha1.Condition + var happyIssuerIsUniqueCondition, + unknownIssuerIsUniqueCondition, + sadIssuerIsUniqueCondition, + happyOneTLSSecretPerIssuerHostnameCondition, + unknownOneTLSSecretPerIssuerHostnameCondition, + sadOneTLSSecretPerIssuerHostnameCondition, + happyIssuerURLValidCondition, + sadIssuerURLValidConditionCannotHaveQuery, + sadIssuerURLValidConditionCannotParse, + sadReadyCondition func(time metav1.Time, observedGeneration int64) v1alpha1.Condition // Defer starting the informers until the last possible moment so that the // nested Before's can keep adding things to the informer caches. @@ -163,6 +174,139 @@ func TestSync(t *testing.T) { Version: v1alpha1.SchemeGroupVersion.Version, Resource: "federationdomains", } + + frozenMetav1Now = metav1.NewTime(frozenNow) + + happyReadyCondition = func(issuer string, time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "Ready", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ + "the discovery endpoint is %s/.well-known/openid-configuration", issuer), + } + } + + sadReadyCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "Ready", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + } + } + + happyIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "spec.issuer is unique among all FederationDomains", + } + } + + unknownIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "Unknown", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "UnableToValidate", + Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", + } + } + + sadIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DuplicateIssuer", + Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", + } + } + + happyOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", + } + } + + unknownOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "Unknown", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "UnableToValidate", + 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", + } + } + + sadOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DifferentSecretRefsFound", + Message: "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found", + } + } + + happyIssuerURLValidCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "spec.issuer is a valid URL", + } + } + + sadIssuerURLValidConditionCannotHaveQuery = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "InvalidIssuerURL", + Message: "issuer must not have query", + } + } + + sadIssuerURLValidConditionCannotParse = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "InvalidIssuerURL", + Message: `could not parse issuer as URL: parse ":/host//path": missing protocol scheme`, + } + } + + allHappyConditions = func(issuer string, time metav1.Time, observedGeneration int64) []v1alpha1.Condition { + return []v1alpha1.Condition{ + happyIssuerIsUniqueCondition(time, observedGeneration), + happyIssuerURLValidCondition(time, observedGeneration), + happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), + happyReadyCondition(issuer, time, observedGeneration), + } + } }) it.After(func() { @@ -177,14 +321,14 @@ func TestSync(t *testing.T) { it.Before(func() { federationDomain1 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain1)) federationDomain2 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain2)) @@ -212,36 +356,24 @@ func TestSync(t *testing.T) { ) }) - it("updates the status to success in the FederationDomains", func() { + it("updates the status to ready in the FederationDomains", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain1.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain1.Status.Message = "Provider successfully created" - federationDomain1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - federationDomain2.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain2.Status.Message = "Provider successfully created" - federationDomain2.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain1.Namespace, - federationDomain1.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", federationDomain1.Namespace, federationDomain1, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain2.Namespace, - federationDomain2.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -254,9 +386,8 @@ func TestSync(t *testing.T) { when("one FederationDomain is already up to date", func() { it.Before(func() { - federationDomain1.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain1.Status.Message = "Provider successfully created" - federationDomain1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) r.NoError(pinnipedAPIClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) r.NoError(pinnipedInformerClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) @@ -267,21 +398,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain2.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain2.Status.Message = "Provider successfully created" - federationDomain2.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain1.Namespace, - federationDomain1.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain2.Namespace, - federationDomain2.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -314,7 +434,7 @@ func TestSync(t *testing.T) { }) }) - when("updating only one FederationDomain fails for a reason other than conflict", func() { + when("updating only one FederationDomain fails", func() { it.Before(func() { once := sync.Once{} pinnipedAPIClient.PrependReactor( @@ -354,31 +474,19 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - federationDomain1.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain1.Status.Message = "Provider successfully created" - federationDomain1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - federationDomain2.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain2.Status.Message = "Provider successfully created" - federationDomain2.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain1.Namespace, - federationDomain1.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", federationDomain1.Namespace, federationDomain1, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain2.Namespace, - federationDomain2.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -398,67 +506,14 @@ func TestSync(t *testing.T) { it.Before(func() { federationDomain = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "config", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) }) - when("there is a conflict while updating an FederationDomain", func() { - it.Before(func() { - once := sync.Once{} - pinnipedAPIClient.PrependReactor( - "update", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { - var err error - once.Do(func() { - err = k8serrors.NewConflict(schema.GroupResource{}, "", nil) - }) - return true, nil, err - }, - ) - }) - - it("retries updating the FederationDomain", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - federationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain.Status.Message = "Provider successfully created" - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) - - expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain.Namespace, - federationDomain, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain.Namespace, - federationDomain, - ), - } - r.Equal(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - - when("updating the FederationDomain fails for a reason other than conflict", func() { + when("updating the FederationDomain fails", func() { it.Before(func() { pinnipedAPIClient.PrependReactor( "update", @@ -474,16 +529,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - federationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain.Status.Message = "Provider successfully created" - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -491,38 +540,7 @@ func TestSync(t *testing.T) { federationDomain, ), } - r.Equal(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - - when("there is an error when getting the FederationDomain", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor( - "get", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("some get error") - }, - ) - }) - - it("returns the get error", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: get failed: some get error") - - federationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain.Status.Message = "Provider successfully created" - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) - - expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), - } - r.Equal(expectedActions, pinnipedAPIClient.Actions()) + r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) }) }) }) @@ -535,14 +553,14 @@ func TestSync(t *testing.T) { it.Before(func() { validFederationDomain = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "valid-config", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "valid-config", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://valid-issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(validFederationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(validFederationDomain)) invalidFederationDomain = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } r.NoError(pinnipedAPIClient.Tracker().Add(invalidFederationDomain)) @@ -566,36 +584,29 @@ func TestSync(t *testing.T) { ) }) - it("updates the status to success/invalid in the FederationDomains", func() { + it("updates the status in each FederationDomain", func() { startInformersAndController() err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - validFederationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - validFederationDomain.Status.Message = "Provider successfully created" - validFederationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + validFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - invalidFederationDomain.Status.Status = v1alpha1.InvalidFederationDomainStatusCondition - invalidFederationDomain.Status.Message = "Invalid: issuer must not have query" - invalidFederationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + invalidFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseError + invalidFederationDomain.Status.Conditions = []v1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - invalidFederationDomain.Namespace, - invalidFederationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", invalidFederationDomain.Namespace, invalidFederationDomain, ), - coretesting.NewGetAction( - federationDomainGVR, - validFederationDomain.Namespace, - validFederationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -606,7 +617,7 @@ func TestSync(t *testing.T) { r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) }) - when("updating only the invalid FederationDomain fails for a reason other than conflict", func() { + when("updating only the invalid FederationDomain fails", func() { it.Before(func() { pinnipedAPIClient.PrependReactor( "update", @@ -645,31 +656,24 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - validFederationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - validFederationDomain.Status.Message = "Provider successfully created" - validFederationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + validFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - invalidFederationDomain.Status.Status = v1alpha1.InvalidFederationDomainStatusCondition - invalidFederationDomain.Status.Message = "Invalid: issuer must not have query" - invalidFederationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + invalidFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseError + invalidFederationDomain.Status.Conditions = []v1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - invalidFederationDomain.Namespace, - invalidFederationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", invalidFederationDomain.Namespace, invalidFederationDomain, ), - coretesting.NewGetAction( - federationDomainGVR, - validFederationDomain.Namespace, - validFederationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -693,20 +697,20 @@ func TestSync(t *testing.T) { // Hostnames are case-insensitive, so consider them to be duplicates if they only differ by case. // Paths are case-sensitive, so having a path that differs only by case makes a new issuer. federationDomainDuplicate1 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate1)) federationDomainDuplicate2 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate2)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate2)) federationDomain = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) @@ -735,46 +739,38 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain.Status.Message = "Provider successfully created" - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) - federationDomainDuplicate1.Status.Status = v1alpha1.DuplicateFederationDomainStatusCondition - federationDomainDuplicate1.Status.Message = "Duplicate issuer: https://iSSueR-duPlicAte.cOm/a" - federationDomainDuplicate1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainDuplicate1.Status.Phase = v1alpha1.FederationDomainPhaseError + federationDomainDuplicate1.Status.Conditions = []v1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } - federationDomainDuplicate2.Status.Status = v1alpha1.DuplicateFederationDomainStatusCondition - federationDomainDuplicate2.Status.Message = "Duplicate issuer: https://issuer-duplicate.com/a" - federationDomainDuplicate2.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainDuplicate2.Status.Phase = v1alpha1.FederationDomainPhaseError + federationDomainDuplicate2.Status.Conditions = []v1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDuplicate1.Namespace, - federationDomainDuplicate1.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", federationDomainDuplicate1.Namespace, federationDomainDuplicate1, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDuplicate2.Namespace, - federationDomainDuplicate2.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", federationDomainDuplicate2.Namespace, federationDomainDuplicate2, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -784,50 +780,6 @@ func TestSync(t *testing.T) { } r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) }) - - when("we cannot talk to the API", func() { - var count int - it.Before(func() { - pinnipedAPIClient.PrependReactor( - "get", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { - count++ - return true, nil, fmt.Errorf("some get error %d", count) - }, - ) - }) - - it("returns the get errors", func() { - expectedError := here.Doc(`[could not update status: get failed: some get error 1, could not update status: get failed: some get error 2, could not update status: get failed: some get error 3]`) - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, expectedError) - - federationDomain.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomain.Status.Message = "Provider successfully created" - federationDomain.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) - - expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDuplicate1.Namespace, - federationDomainDuplicate1.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDuplicate2.Namespace, - federationDomainDuplicate2.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomain.Namespace, - federationDomain.Name, - ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) }) when("there are FederationDomains with the same issuer DNS hostname using different secretNames", func() { @@ -840,7 +792,7 @@ func TestSync(t *testing.T) { it.Before(func() { federationDomainSameIssuerAddress1 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{ Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -849,7 +801,7 @@ func TestSync(t *testing.T) { r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) federationDomainSameIssuerAddress2 = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{ // Validation treats these as the same DNS hostname even though they have different port numbers, // because SNI information on the incoming requests is not going to include port numbers. @@ -861,7 +813,7 @@ func TestSync(t *testing.T) { r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) federationDomainDifferentIssuerAddress = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{ Issuer: "https://issuer-not-duplicate.com", TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -876,7 +828,7 @@ func TestSync(t *testing.T) { _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. r.Error(err) federationDomainWithInvalidIssuerURL = &v1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, Spec: v1alpha1.FederationDomainSpec{ Issuer: invalidIssuerURL, TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, @@ -908,27 +860,39 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomainDifferentIssuerAddress.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomainDifferentIssuerAddress.Status.Message = "Provider successfully created" - federationDomainDifferentIssuerAddress.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainDifferentIssuerAddress.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomainDifferentIssuerAddress.Status.Conditions = allHappyConditions(federationDomainDifferentIssuerAddress.Spec.Issuer, frozenMetav1Now, 123) - federationDomainSameIssuerAddress1.Status.Status = v1alpha1.SameIssuerHostMustUseSameSecretFederationDomainStatusCondition - federationDomainSameIssuerAddress1.Status.Message = "Issuers with the same DNS hostname (address not including port) must use the same secretName: issuer-duplicate-address.com" - federationDomainSameIssuerAddress1.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainSameIssuerAddress1.Status.Phase = v1alpha1.FederationDomainPhaseError + federationDomainSameIssuerAddress1.Status.Conditions = []v1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } - federationDomainSameIssuerAddress2.Status.Status = v1alpha1.SameIssuerHostMustUseSameSecretFederationDomainStatusCondition - federationDomainSameIssuerAddress2.Status.Message = "Issuers with the same DNS hostname (address not including port) must use the same secretName: issuer-duplicate-address.com" - federationDomainSameIssuerAddress2.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainSameIssuerAddress2.Status.Phase = v1alpha1.FederationDomainPhaseError + federationDomainSameIssuerAddress2.Status.Conditions = []v1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } - federationDomainWithInvalidIssuerURL.Status.Status = v1alpha1.InvalidFederationDomainStatusCondition - federationDomainWithInvalidIssuerURL.Status.Message = `Invalid: could not parse issuer as URL: parse ":/host//path": missing protocol scheme` - federationDomainWithInvalidIssuerURL.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) + federationDomainWithInvalidIssuerURL.Status.Phase = v1alpha1.FederationDomainPhaseError + federationDomainWithInvalidIssuerURL.Status.Conditions = []v1alpha1.Condition{ + unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), + unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + } expectedActions := []coretesting.Action{ - coretesting.NewGetAction( + coretesting.NewUpdateSubresourceAction( federationDomainGVR, - federationDomainSameIssuerAddress1.Namespace, - federationDomainSameIssuerAddress1.Name, + "status", + federationDomainDifferentIssuerAddress.Namespace, + federationDomainDifferentIssuerAddress, ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, @@ -936,33 +900,12 @@ func TestSync(t *testing.T) { federationDomainSameIssuerAddress1.Namespace, federationDomainSameIssuerAddress1, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainSameIssuerAddress2.Namespace, - federationDomainSameIssuerAddress2.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", federationDomainSameIssuerAddress2.Namespace, federationDomainSameIssuerAddress2, ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDifferentIssuerAddress.Namespace, - federationDomainDifferentIssuerAddress.Name, - ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainDifferentIssuerAddress.Namespace, - federationDomainDifferentIssuerAddress, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainWithInvalidIssuerURL.Namespace, - federationDomainWithInvalidIssuerURL.Name, - ), coretesting.NewUpdateSubresourceAction( federationDomainGVR, "status", @@ -972,55 +915,6 @@ func TestSync(t *testing.T) { } r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) }) - - when("we cannot talk to the API", func() { - var count int - it.Before(func() { - pinnipedAPIClient.PrependReactor( - "get", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { - count++ - return true, nil, fmt.Errorf("some get error %d", count) - }, - ) - }) - - it("returns the get errors", func() { - expectedError := here.Doc(`[could not update status: get failed: some get error 1, could not update status: get failed: some get error 2, could not update status: get failed: some get error 3, could not update status: get failed: some get error 4]`) - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, expectedError) - - federationDomainDifferentIssuerAddress.Status.Status = v1alpha1.SuccessFederationDomainStatusCondition - federationDomainDifferentIssuerAddress.Status.Message = "Provider successfully created" - federationDomainDifferentIssuerAddress.Status.LastUpdateTime = timePtr(metav1.NewTime(frozenNow)) - - expectedActions := []coretesting.Action{ - coretesting.NewGetAction( - federationDomainGVR, - federationDomainSameIssuerAddress1.Namespace, - federationDomainSameIssuerAddress1.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainSameIssuerAddress2.Namespace, - federationDomainSameIssuerAddress2.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainDifferentIssuerAddress.Namespace, - federationDomainDifferentIssuerAddress.Name, - ), - coretesting.NewGetAction( - federationDomainGVR, - federationDomainWithInvalidIssuerURL.Namespace, - federationDomainWithInvalidIssuerURL.Name, - ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) }) when("there are no FederationDomains in the informer", func() { diff --git a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go index 3209a7f27..918f91e51 100644 --- a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go +++ b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go @@ -133,11 +133,12 @@ func (c *oidcClientWatcherController) updateStatus( ) error { updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.MergeConfigConditions(conditions, upstream.Generation, &updated.Status.Conditions, plog.New()) + hadErrorCondition := conditionsutil.MergeConfigConditions(conditions, + upstream.Generation, &updated.Status.Conditions, plog.New(), metav1.Now()) - updated.Status.Phase = v1alpha1.PhaseReady + updated.Status.Phase = v1alpha1.OIDCClientPhaseReady if hadErrorCondition { - updated.Status.Phase = v1alpha1.PhaseError + updated.Status.Phase = v1alpha1.OIDCClientPhaseError } updated.Status.TotalClientSecrets = int32(totalClientSecrets) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 98df33dee..5e8c800ba 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -103,7 +103,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { downstream := testlib.CreateTestFederationDomain(topSetupCtx, t, issuerURL.String(), certSecret.Name, - configv1alpha1.SuccessFederationDomainStatusCondition, + configv1alpha1.FederationDomainPhaseReady, ) // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 2884c7153..08727e6d4 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -441,7 +441,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr // over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all // Pinniped API resources. Without this, the test could accidentally skip parts of the tree if the // format has changed. - require.Equal(t, 254, foundFieldNames, + require.Equal(t, 259, foundFieldNames, "Expected to find all known fields of all Pinniped API resources. "+ "You may will need to update this expectation if you added new fields to the API types.", ) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 9813d9585..d591c139c 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -125,14 +125,24 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving. config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, issuer6, "", "") - requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.DuplicateFederationDomainStatusCondition) - requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.DuplicateFederationDomainStatusCondition) + requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionFalse, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + }) + requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionFalse, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) // If we delete the first duplicate issuer, the second duplicate issuer starts serving. requireDelete(t, client, ns, config6Duplicate1.Name) requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6, nil) - requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, client, ns, config6Duplicate2.Name) // When we finally delete all issuers, the endpoint should be down. requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config6Duplicate2, client, ns, scheme, addr, caBundle, issuer6) @@ -144,7 +154,12 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When we create a provider with an invalid issuer, the status is set to invalid. badConfig := testlib.CreateTestFederationDomain(ctx, t, badIssuer, "", "") - requireStatus(t, client, ns, badConfig.Name, v1alpha1.InvalidFederationDomainStatusCondition) + requireStatus(t, client, ns, badConfig.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionFalse, + }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) }) @@ -172,7 +187,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { // Create an FederationDomain with a spec.tls.secretName. federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, issuer1, certSecretName1, "") - requireStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) // The spec.tls.secretName Secret does not exist, so the endpoints should fail with TLS errors. requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady(t, issuer1) @@ -212,7 +227,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { // Create an FederationDomain with a spec.tls.secretName. federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, issuer2, certSecretName2, "") - requireStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) // Create the Secret. ca2 := createTLSCertificateSecret(ctx, t, ns, hostname2, nil, certSecretName2, kubeClient) @@ -256,7 +271,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { // Create an FederationDomain without a spec.tls.secretName. federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, issuerUsingIPAddress, "", "") - requireStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) // There is no default TLS cert and the spec.tls.secretName was not set, so the endpoints should fail with TLS errors. requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady(t, issuerUsingIPAddress) @@ -270,7 +285,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { // Create an FederationDomain with a spec.tls.secretName. certSecretName := "integration-test-cert-1" federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, issuerUsingHostname, certSecretName, "") - requireStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) // Create the Secret. certCA := createTLSCertificateSecret(ctx, t, ns, hostname, nil, certSecretName, kubeClient) @@ -458,7 +473,7 @@ func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear( t.Helper() newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, issuerName, "", "") jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) - requireStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name, v1alpha1.SuccessFederationDomainStatusCondition) + requireFullySuccessfulStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name) return newFederationDomain, jwksResult } @@ -626,7 +641,16 @@ func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name st require.NoError(t, err) } -func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, status v1alpha1.FederationDomainStatusCondition) { +func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string) { + requireStatus(t, client, ns, name, v1alpha1.FederationDomainPhaseReady, map[string]v1alpha1.ConditionStatus{ + "Ready": v1alpha1.ConditionTrue, + "IssuerIsUnique": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + }) +} + +func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, phase v1alpha1.FederationDomainPhase, conditionTypeToStatus map[string]v1alpha1.ConditionStatus) { t.Helper() testlib.RequireEventually(t, func(requireEventually *require.Assertions) { @@ -636,8 +660,14 @@ func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name st federationDomain, err := client.ConfigV1alpha1().FederationDomains(ns).Get(ctx, name, metav1.GetOptions{}) requireEventually.NoError(err) - t.Logf("found FederationDomain %s/%s with status %s", ns, name, federationDomain.Status.Status) - requireEventually.Equalf(status, federationDomain.Status.Status, "unexpected status (message = '%s')", federationDomain.Status.Message) + t.Logf("found FederationDomain %s/%s with phase %s", ns, name, federationDomain.Status.Phase) + requireEventually.Equalf(phase, federationDomain.Status.Phase, "unexpected phase (conditions = '%#v')", federationDomain.Status.Conditions) + + actualConditionTypeToStatus := map[string]v1alpha1.ConditionStatus{} + for _, c := range federationDomain.Status.Conditions { + actualConditionTypeToStatus[c.Type] = c.Status + } + requireEventually.Equal(conditionTypeToStatus, actualConditionTypeToStatus, "unexpected statuses for conditions by type") }, 5*time.Minute, 200*time.Millisecond) } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 3bfd780d3..8b4728100 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -1469,7 +1469,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, // the ID token Subject should include the upstream user ID after the upstream issuer name @@ -1502,7 +1502,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, // the ID token Subject should include the upstream user ID after the upstream issuer name @@ -1526,7 +1526,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1558,7 +1558,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange grant type not allowed AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"}, // a validation requires that we also disallow the pinniped:request-audience scope - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1592,7 +1592,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1626,7 +1626,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1652,7 +1652,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1678,7 +1678,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1711,7 +1711,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1750,7 +1750,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access"}, // validations require that when username/groups are excluded, then token exchange must also not be allowed - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, testUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -1789,7 +1789,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) }, requestAuthorization: func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t, @@ -1825,7 +1825,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, - }, configv1alpha1.PhaseReady) + }, configv1alpha1.OIDCClientPhaseReady) return clientID, "wrong-client-secret" }, testUser: func(t *testing.T) (string, string) { @@ -2080,7 +2080,7 @@ func testSupervisorLogin( downstream := testlib.CreateTestFederationDomain(ctx, t, issuerURL.String(), certSecret.Name, - configv1alpha1.SuccessFederationDomainStatusCondition, + configv1alpha1.FederationDomainPhaseReady, // TODO: expect another phase because this is a legacy FederationDomain and there is no IDP yet, so it is not safe to try to do logins until the IDP exists and the controller has a chance to run again to set the default IDP ) // Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for @@ -2104,6 +2104,9 @@ func testSupervisorLogin( // Create upstream IDP and wait for it to become ready. idpName := createIDP(t) + // Now that both the FederationDomain and the IDP are created, the FederationDomain should be ready. + testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + // Start a callback server on localhost. localCallbackServer := startLocalCallbackServer(t) diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index a025c8887..6068ee8c4 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -85,7 +85,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { downstream := testlib.CreateTestFederationDomain(ctx, t, issuerURL.String(), certSecret.Name, - configv1alpha1.SuccessFederationDomainStatusCondition, + configv1alpha1.FederationDomainPhaseReady, ) // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. diff --git a/test/testlib/client.go b/test/testlib/client.go index 55f208c11..4ab526a52 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -272,7 +272,13 @@ func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alp // current test's lifetime. // If the provided issuer is not the empty string, then it will be used for the // FederationDomain.Spec.Issuer field. Else, a random issuer will be generated. -func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string, certSecretName string, expectStatus configv1alpha1.FederationDomainStatusCondition) *configv1alpha1.FederationDomain { +func CreateTestFederationDomain( + ctx context.Context, + t *testing.T, + issuer string, + certSecretName string, + expectStatus configv1alpha1.FederationDomainPhase, +) *configv1alpha1.FederationDomain { t.Helper() testEnv := IntegrationEnv(t) @@ -283,8 +289,8 @@ func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string issuer = fmt.Sprintf("http://test-issuer-%s.pinniped.dev", RandHex(t, 8)) } - federationDomains := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) - federationDomain, err := federationDomains.Create(createContext, &configv1alpha1.FederationDomain{ + federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) + federationDomain, err := federationDomainsClient.Create(createContext, &configv1alpha1.FederationDomain{ ObjectMeta: testObjectMeta(t, "oidc-provider"), Spec: configv1alpha1.FederationDomainSpec{ Issuer: issuer, @@ -299,7 +305,7 @@ func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string t.Logf("cleaning up test FederationDomain %s/%s", federationDomain.Namespace, federationDomain.Name) deleteCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - err := federationDomains.Delete(deleteCtx, federationDomain.Name, metav1.DeleteOptions{}) + err := federationDomainsClient.Delete(deleteCtx, federationDomain.Name, metav1.DeleteOptions{}) notFound := k8serrors.IsNotFound(err) // It's okay if it is not found, because it might have been deleted by another part of this test. if !notFound { @@ -313,22 +319,31 @@ func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string } // Wait for the FederationDomain to enter the expected phase (or time out). + WaitForTestFederationDomainStatus(ctx, t, federationDomain.Name, expectStatus) + + return federationDomain +} + +func WaitForTestFederationDomainStatus(ctx context.Context, t *testing.T, federationDomainName string, expectStatus configv1alpha1.FederationDomainPhase) { + t.Helper() + testEnv := IntegrationEnv(t) + federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) + var result *configv1alpha1.FederationDomain RequireEventuallyf(t, func(requireEventually *require.Assertions) { var err error - result, err = federationDomains.Get(ctx, federationDomain.Name, metav1.GetOptions{}) + result, err = federationDomainsClient.Get(ctx, federationDomainName, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventually.Equal(expectStatus, result.Status.Status) + requireEventually.Equal(expectStatus, result.Status.Phase) // If the FederationDomain was successfully created, ensure all secrets are present before continuing - if expectStatus == configv1alpha1.SuccessFederationDomainStatusCondition { + if expectStatus == configv1alpha1.FederationDomainPhaseReady { requireEventually.NotEmpty(result.Status.Secrets.JWKS.Name, "expected status.secrets.jwks.name not to be empty") requireEventually.NotEmpty(result.Status.Secrets.TokenSigningKey.Name, "expected status.secrets.tokenSigningKey.name not to be empty") requireEventually.NotEmpty(result.Status.Secrets.StateSigningKey.Name, "expected status.secrets.stateSigningKey.name not to be empty") requireEventually.NotEmpty(result.Status.Secrets.StateEncryptionKey.Name, "expected status.secrets.stateEncryptionKey.name not to be empty") } }, 60*time.Second, 1*time.Second, "expected the FederationDomain to have status %q", expectStatus) - return federationDomain } func RandBytes(t *testing.T, numBytes int) []byte { From 3521e129cde2cccd86691f30beffc0b4bb53993c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 5 Jul 2023 09:30:02 -0700 Subject: [PATCH 26/81] Change name of FederationDomain printer column back to "Status" To be consistent with the name of the pinter columns on our other CRDs, which call the Phase "Status" in the printer column names. --- apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl | 2 +- .../config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- .../crds/config.supervisor.pinniped.dev_federationdomains.yaml | 2 +- .../apis/supervisor/config/v1alpha1/types_federationdomain.go | 2 +- test/integration/kube_api_discovery_test.go | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl index 0290c618b..e71ab0829 100644 --- a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index cb7e1a519..28a7425e9 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go index 0290c618b..e71ab0829 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml index cb7e1a519..28a7425e9 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go index 0290c618b..e71ab0829 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml index cb7e1a519..28a7425e9 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go index 0290c618b..e71ab0829 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml index cb7e1a519..28a7425e9 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age 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 0290c618b..e71ab0829 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { 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 cb7e1a519..28a7425e9 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age 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 0290c618b..e71ab0829 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { 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 cb7e1a519..28a7425e9 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age 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 0290c618b..e71ab0829 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { 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 cb7e1a519..28a7425e9 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age 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 0290c618b..e71ab0829 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { 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 cb7e1a519..28a7425e9 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -22,7 +22,7 @@ spec: name: Issuer type: string - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .metadata.creationTimestamp name: Age diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go index 0290c618b..e71ab0829 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -288,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 08727e6d4..8d2e7267f 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -558,7 +558,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { addSuffix("federationdomains.config.supervisor"): { "v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{ {Name: "Issuer", Type: "string", JSONPath: ".spec.issuer"}, - {Name: "Status", Type: "string", JSONPath: ".status.status"}, + {Name: "Status", Type: "string", JSONPath: ".status.phase"}, {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, }, }, From 5e2f98af6567b602d44ce24ecb37feb4aa5fbc18 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 5 Jul 2023 13:33:21 -0700 Subject: [PATCH 27/81] Update informers unit test for FederationDomainWatcherController --- .../federation_domain_watcher_test.go | 306 ++++++++++-------- 1 file changed, 164 insertions(+), 142 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index f274424a2..fecb503e9 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -16,13 +16,15 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" "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/runtime/schema" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" @@ -30,20 +32,63 @@ import ( "go.pinniped.dev/internal/testutil" ) -func TestInformerFilters(t *testing.T) { - spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) { - var r *require.Assertions - var observableWithInformerOption *testutil.ObservableWithInformerOption - var configMapInformerFilter controllerlib.Filter - - it.Before(func() { - r = require.New(t) - observableWithInformerOption = testutil.NewObservableWithInformerOption() - federationDomainInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).Config().V1alpha1().FederationDomains() - oidcIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders() - ldapIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders() - adIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders() - _ = NewFederationDomainWatcherController( +func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { + t.Parallel() + + federationDomainInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).Config().V1alpha1().FederationDomains() + oidcIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders() + ldapIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders() + adIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders() + + tests := []struct { + name string + obj metav1.Object + informer controllerlib.InformerGetter + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any FederationDomain changes", + obj: &configv1alpha1.FederationDomain{}, + informer: federationDomainInformer, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "any OIDCIdentityProvider adds or deletes, but updates are ignored", + obj: &idpv1alpha1.OIDCIdentityProvider{}, + informer: oidcIdentityProviderInformer, + wantAdd: true, + wantUpdate: false, + wantDelete: true, + }, + { + name: "any LDAPIdentityProvider adds or deletes, but updates are ignored", + obj: &idpv1alpha1.LDAPIdentityProvider{}, + informer: ldapIdentityProviderInformer, + wantAdd: true, + wantUpdate: false, + wantDelete: true, + }, + { + name: "any ActiveDirectoryIdentityProvider adds or deletes, but updates are ignored", + obj: &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + informer: adIdentityProviderInformer, + wantAdd: true, + wantUpdate: false, + wantDelete: true, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + withInformer := testutil.NewObservableWithInformerOption() + + NewFederationDomainWatcherController( nil, nil, nil, @@ -51,40 +96,17 @@ func TestInformerFilters(t *testing.T) { oidcIdentityProviderInformer, ldapIdentityProviderInformer, adIdentityProviderInformer, - observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters + withInformer.WithInformer, // make it possible to observe the behavior of the Filters ) - configMapInformerFilter = observableWithInformerOption.GetFilterForInformer(federationDomainInformer) - }) - - when("watching FederationDomain objects", func() { - var subject controllerlib.Filter - var target, otherNamespace, otherName *v1alpha1.FederationDomain - it.Before(func() { - subject = configMapInformerFilter - target = &v1alpha1.FederationDomain{ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}} - otherNamespace = &v1alpha1.FederationDomain{ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "other-namespace"}} - otherName = &v1alpha1.FederationDomain{ObjectMeta: metav1.ObjectMeta{Name: "other-name", Namespace: "some-namespace"}} - }) - - when("any FederationDomain changes", func() { - it("returns true to trigger the sync method", func() { - r.True(subject.Add(target)) - r.True(subject.Add(otherName)) - r.True(subject.Add(otherNamespace)) - r.True(subject.Update(target, otherName)) - r.True(subject.Update(otherName, otherName)) - r.True(subject.Update(otherNamespace, otherName)) - r.True(subject.Update(otherName, target)) - r.True(subject.Update(otherName, otherName)) - r.True(subject.Update(otherName, otherNamespace)) - r.True(subject.Delete(target)) - r.True(subject.Delete(otherName)) - r.True(subject.Delete(otherNamespace)) - }) - }) + unrelatedObj := corev1.Secret{} + filter := withInformer.GetFilterForInformer(test.informer) + require.Equal(t, test.wantAdd, filter.Add(test.obj)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelatedObj, test.obj)) + require.Equal(t, test.wantUpdate, filter.Update(test.obj, &unrelatedObj)) + require.Equal(t, test.wantDelete, filter.Delete(test.obj)) }) - }, spec.Parallel(), spec.Report(report.Terminal{})) + } } type fakeFederationDomainsSetter struct { @@ -97,7 +119,7 @@ func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ... f.FederationDomainsReceived = federationDomains } -func TestSync(t *testing.T) { +func TestTestFederationDomainWatcherControllerSync(t *testing.T) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { const namespace = "some-namespace" @@ -114,8 +136,8 @@ func TestSync(t *testing.T) { var frozenMetav1Now metav1.Time var federationDomainsSetter *fakeFederationDomainsSetter var federationDomainGVR schema.GroupVersionResource - var allHappyConditions func(issuer string, time metav1.Time, observedGeneration int64) []v1alpha1.Condition - var happyReadyCondition func(issuer string, time metav1.Time, observedGeneration int64) v1alpha1.Condition + var allHappyConditions func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition + var happyReadyCondition func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition var happyIssuerIsUniqueCondition, unknownIssuerIsUniqueCondition, sadIssuerIsUniqueCondition, @@ -125,7 +147,7 @@ func TestSync(t *testing.T) { happyIssuerURLValidCondition, sadIssuerURLValidConditionCannotHaveQuery, sadIssuerURLValidConditionCannotParse, - sadReadyCondition func(time metav1.Time, observedGeneration int64) v1alpha1.Condition + sadReadyCondition func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition // Defer starting the informers until the last possible moment so that the // nested Before's can keep adding things to the informer caches. @@ -170,15 +192,15 @@ func TestSync(t *testing.T) { pinnipedAPIClient = pinnipedfake.NewSimpleClientset() federationDomainGVR = schema.GroupVersionResource{ - Group: v1alpha1.SchemeGroupVersion.Group, - Version: v1alpha1.SchemeGroupVersion.Version, + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, Resource: "federationdomains", } frozenMetav1Now = metav1.NewTime(frozenNow) - happyReadyCondition = func(issuer string, time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + happyReadyCondition = func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "Ready", Status: "True", ObservedGeneration: observedGeneration, @@ -189,8 +211,8 @@ func TestSync(t *testing.T) { } } - sadReadyCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + sadReadyCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "Ready", Status: "False", ObservedGeneration: observedGeneration, @@ -200,8 +222,8 @@ func TestSync(t *testing.T) { } } - happyIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + happyIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "True", ObservedGeneration: observedGeneration, @@ -211,8 +233,8 @@ func TestSync(t *testing.T) { } } - unknownIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + unknownIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "Unknown", ObservedGeneration: observedGeneration, @@ -222,8 +244,8 @@ func TestSync(t *testing.T) { } } - sadIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + sadIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerIsUnique", Status: "False", ObservedGeneration: observedGeneration, @@ -233,8 +255,8 @@ func TestSync(t *testing.T) { } } - happyOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + happyOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "True", ObservedGeneration: observedGeneration, @@ -244,8 +266,8 @@ func TestSync(t *testing.T) { } } - unknownOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + unknownOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "Unknown", ObservedGeneration: observedGeneration, @@ -255,8 +277,8 @@ func TestSync(t *testing.T) { } } - sadOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + sadOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "False", ObservedGeneration: observedGeneration, @@ -266,8 +288,8 @@ func TestSync(t *testing.T) { } } - happyIssuerURLValidCondition = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + happyIssuerURLValidCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "True", ObservedGeneration: observedGeneration, @@ -277,8 +299,8 @@ func TestSync(t *testing.T) { } } - sadIssuerURLValidConditionCannotHaveQuery = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + sadIssuerURLValidConditionCannotHaveQuery = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, @@ -288,8 +310,8 @@ func TestSync(t *testing.T) { } } - sadIssuerURLValidConditionCannotParse = func(time metav1.Time, observedGeneration int64) v1alpha1.Condition { - return v1alpha1.Condition{ + sadIssuerURLValidConditionCannotParse = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, @@ -299,8 +321,8 @@ func TestSync(t *testing.T) { } } - allHappyConditions = func(issuer string, time metav1.Time, observedGeneration int64) []v1alpha1.Condition { - return []v1alpha1.Condition{ + allHappyConditions = func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + return []configv1alpha1.Condition{ happyIssuerIsUniqueCondition(time, observedGeneration), happyIssuerURLValidCondition(time, observedGeneration), happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), @@ -315,21 +337,21 @@ func TestSync(t *testing.T) { when("there are some valid FederationDomains in the informer", func() { var ( - federationDomain1 *v1alpha1.FederationDomain - federationDomain2 *v1alpha1.FederationDomain + federationDomain1 *configv1alpha1.FederationDomain + federationDomain2 *configv1alpha1.FederationDomain ) it.Before(func() { - federationDomain1 = &v1alpha1.FederationDomain{ + federationDomain1 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain1)) - federationDomain2 = &v1alpha1.FederationDomain{ + federationDomain2 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain2)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain2)) @@ -361,10 +383,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ @@ -386,7 +408,7 @@ func TestSync(t *testing.T) { when("one FederationDomain is already up to date", func() { it.Before(func() { - federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) r.NoError(pinnipedAPIClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) @@ -398,7 +420,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ @@ -474,10 +496,10 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - federationDomain1.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - federationDomain2.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ @@ -501,13 +523,13 @@ func TestSync(t *testing.T) { when("there are errors updating the FederationDomains", func() { var ( - federationDomain *v1alpha1.FederationDomain + federationDomain *configv1alpha1.FederationDomain ) it.Before(func() { - federationDomain = &v1alpha1.FederationDomain{ + federationDomain = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer.com"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) @@ -529,7 +551,7 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - federationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) expectedActions := []coretesting.Action{ @@ -547,21 +569,21 @@ func TestSync(t *testing.T) { when("there are both valid and invalid FederationDomains in the informer", func() { var ( - validFederationDomain *v1alpha1.FederationDomain - invalidFederationDomain *v1alpha1.FederationDomain + validFederationDomain *configv1alpha1.FederationDomain + invalidFederationDomain *configv1alpha1.FederationDomain ) it.Before(func() { - validFederationDomain = &v1alpha1.FederationDomain{ + validFederationDomain = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "valid-config", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://valid-issuer.com"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://valid-issuer.com"}, } r.NoError(pinnipedAPIClient.Tracker().Add(validFederationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(validFederationDomain)) - invalidFederationDomain = &v1alpha1.FederationDomain{ + invalidFederationDomain = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } r.NoError(pinnipedAPIClient.Tracker().Add(invalidFederationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(invalidFederationDomain)) @@ -589,11 +611,11 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - validFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + validFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - invalidFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseError - invalidFederationDomain.Status.Conditions = []v1alpha1.Condition{ + invalidFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseError + invalidFederationDomain.Status.Conditions = []configv1alpha1.Condition{ happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -624,7 +646,7 @@ func TestSync(t *testing.T) { "federationdomains", func(action coretesting.Action) (bool, runtime.Object, error) { updateAction := action.(coretesting.UpdateActionImpl) - federationDomain := updateAction.Object.(*v1alpha1.FederationDomain) + federationDomain := updateAction.Object.(*configv1alpha1.FederationDomain) if federationDomain.Name == validFederationDomain.Name { return true, nil, nil } @@ -656,11 +678,11 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update status: some update error") - validFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + validFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - invalidFederationDomain.Status.Phase = v1alpha1.FederationDomainPhaseError - invalidFederationDomain.Status.Conditions = []v1alpha1.Condition{ + invalidFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseError + invalidFederationDomain.Status.Conditions = []configv1alpha1.Condition{ happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -688,30 +710,30 @@ func TestSync(t *testing.T) { when("there are FederationDomains with duplicate issuer names in the informer", func() { var ( - federationDomainDuplicate1 *v1alpha1.FederationDomain - federationDomainDuplicate2 *v1alpha1.FederationDomain - federationDomain *v1alpha1.FederationDomain + federationDomainDuplicate1 *configv1alpha1.FederationDomain + federationDomainDuplicate2 *configv1alpha1.FederationDomain + federationDomain *configv1alpha1.FederationDomain ) it.Before(func() { // Hostnames are case-insensitive, so consider them to be duplicates if they only differ by case. // Paths are case-sensitive, so having a path that differs only by case makes a new issuer. - federationDomainDuplicate1 = &v1alpha1.FederationDomain{ + federationDomainDuplicate1 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate1)) - federationDomainDuplicate2 = &v1alpha1.FederationDomain{ + federationDomainDuplicate2 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate2)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate2)) - federationDomain = &v1alpha1.FederationDomain{ + federationDomain = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) @@ -739,19 +761,19 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomain.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) - federationDomainDuplicate1.Status.Phase = v1alpha1.FederationDomainPhaseError - federationDomainDuplicate1.Status.Conditions = []v1alpha1.Condition{ + federationDomainDuplicate1.Status.Phase = configv1alpha1.FederationDomainPhaseError + federationDomainDuplicate1.Status.Conditions = []configv1alpha1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), } - federationDomainDuplicate2.Status.Phase = v1alpha1.FederationDomainPhaseError - federationDomainDuplicate2.Status.Conditions = []v1alpha1.Condition{ + federationDomainDuplicate2.Status.Phase = configv1alpha1.FederationDomainPhaseError + federationDomainDuplicate2.Status.Conditions = []configv1alpha1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -784,39 +806,39 @@ func TestSync(t *testing.T) { when("there are FederationDomains with the same issuer DNS hostname using different secretNames", func() { var ( - federationDomainSameIssuerAddress1 *v1alpha1.FederationDomain - federationDomainSameIssuerAddress2 *v1alpha1.FederationDomain - federationDomainDifferentIssuerAddress *v1alpha1.FederationDomain - federationDomainWithInvalidIssuerURL *v1alpha1.FederationDomain + federationDomainSameIssuerAddress1 *configv1alpha1.FederationDomain + federationDomainSameIssuerAddress2 *configv1alpha1.FederationDomain + federationDomainDifferentIssuerAddress *configv1alpha1.FederationDomain + federationDomainWithInvalidIssuerURL *configv1alpha1.FederationDomain ) it.Before(func() { - federationDomainSameIssuerAddress1 = &v1alpha1.FederationDomain{ + federationDomainSameIssuerAddress1 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{ + Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", - TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress1)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) - federationDomainSameIssuerAddress2 = &v1alpha1.FederationDomain{ + federationDomainSameIssuerAddress2 = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{ + Spec: configv1alpha1.FederationDomainSpec{ // Validation treats these as the same DNS hostname even though they have different port numbers, // because SNI information on the incoming requests is not going to include port numbers. Issuer: "https://issuer-duplicate-address.com:1234/path2", - TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress2)) r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) - federationDomainDifferentIssuerAddress = &v1alpha1.FederationDomain{ + federationDomainDifferentIssuerAddress = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{ + Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer-not-duplicate.com", - TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDifferentIssuerAddress)) @@ -827,11 +849,11 @@ func TestSync(t *testing.T) { invalidIssuerURL := ":/host//path" _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. r.Error(err) - federationDomainWithInvalidIssuerURL = &v1alpha1.FederationDomain{ + federationDomainWithInvalidIssuerURL = &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, - Spec: v1alpha1.FederationDomainSpec{ + Spec: configv1alpha1.FederationDomainSpec{ Issuer: invalidIssuerURL, - TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, } r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) @@ -860,27 +882,27 @@ func TestSync(t *testing.T) { err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) - federationDomainDifferentIssuerAddress.Status.Phase = v1alpha1.FederationDomainPhaseReady + federationDomainDifferentIssuerAddress.Status.Phase = configv1alpha1.FederationDomainPhaseReady federationDomainDifferentIssuerAddress.Status.Conditions = allHappyConditions(federationDomainDifferentIssuerAddress.Spec.Issuer, frozenMetav1Now, 123) - federationDomainSameIssuerAddress1.Status.Phase = v1alpha1.FederationDomainPhaseError - federationDomainSameIssuerAddress1.Status.Conditions = []v1alpha1.Condition{ + federationDomainSameIssuerAddress1.Status.Phase = configv1alpha1.FederationDomainPhaseError + federationDomainSameIssuerAddress1.Status.Conditions = []configv1alpha1.Condition{ happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), } - federationDomainSameIssuerAddress2.Status.Phase = v1alpha1.FederationDomainPhaseError - federationDomainSameIssuerAddress2.Status.Conditions = []v1alpha1.Condition{ + federationDomainSameIssuerAddress2.Status.Phase = configv1alpha1.FederationDomainPhaseError + federationDomainSameIssuerAddress2.Status.Conditions = []configv1alpha1.Condition{ happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), } - federationDomainWithInvalidIssuerURL.Status.Phase = v1alpha1.FederationDomainPhaseError - federationDomainWithInvalidIssuerURL.Status.Conditions = []v1alpha1.Condition{ + federationDomainWithInvalidIssuerURL.Status.Phase = configv1alpha1.FederationDomainPhaseError + federationDomainWithInvalidIssuerURL.Status.Conditions = []configv1alpha1.Condition{ unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), From 48e44e13c6c9f18b0a54bdc62ecd4f1a6d61abf7 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 5 Jul 2023 17:41:27 -0700 Subject: [PATCH 28/81] Change federation_domain_watcher_test.go to use a test table style --- .../federation_domain_watcher_test.go | 1303 +++++++---------- 1 file changed, 547 insertions(+), 756 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index fecb503e9..942f6c418 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -8,13 +8,9 @@ import ( "errors" "fmt" "net/url" - "reflect" - "sync" "testing" "time" - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -120,709 +116,468 @@ func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ... } func TestTestFederationDomainWatcherControllerSync(t *testing.T) { - spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { - const namespace = "some-namespace" - - var r *require.Assertions - - var subject controllerlib.Controller - var pinnipedInformerClient *pinnipedfake.Clientset - var pinnipedInformers pinnipedinformers.SharedInformerFactory - var pinnipedAPIClient *pinnipedfake.Clientset - var cancelContext context.Context - var cancelContextCancelFunc context.CancelFunc - var syncContext *controllerlib.Context - var frozenNow time.Time - var frozenMetav1Now metav1.Time - var federationDomainsSetter *fakeFederationDomainsSetter - var federationDomainGVR schema.GroupVersionResource - var allHappyConditions func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition - var happyReadyCondition func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition - var happyIssuerIsUniqueCondition, - unknownIssuerIsUniqueCondition, - sadIssuerIsUniqueCondition, - happyOneTLSSecretPerIssuerHostnameCondition, - unknownOneTLSSecretPerIssuerHostnameCondition, - sadOneTLSSecretPerIssuerHostnameCondition, - happyIssuerURLValidCondition, - sadIssuerURLValidConditionCannotHaveQuery, - sadIssuerURLValidConditionCannotParse, - sadReadyCondition func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition - - // Defer starting the informers until the last possible moment so that the - // nested Before's can keep adding things to the informer caches. - var startInformersAndController = func() { - // Set this at the last second to allow for injection of server override. - subject = NewFederationDomainWatcherController( - federationDomainsSetter, - clocktesting.NewFakeClock(frozenNow), - pinnipedAPIClient, - pinnipedInformers.Config().V1alpha1().FederationDomains(), - pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), - pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), - pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), - controllerlib.WithInformer, - ) + t.Parallel() - // Set this at the last second to support calling subject.Name(). - syncContext = &controllerlib.Context{ - Context: cancelContext, - Name: subject.Name(), - Key: controllerlib.Key{ - Namespace: namespace, - Name: "config-name", - }, - } + const namespace = "some-namespace" - // Must start informers before calling TestRunSynchronously() - pinnipedInformers.Start(cancelContext.Done()) - controllerlib.TestRunSynchronously(t, subject) - } + frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) + frozenMetav1Now := metav1.NewTime(frozenNow) - it.Before(func() { - r = require.New(t) + federationDomainGVR := schema.GroupVersionResource{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Resource: "federationdomains", + } - federationDomainsSetter = &fakeFederationDomainsSetter{} - frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) + federationDomain1 := &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, + } - cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background()) + federationDomain1Issuer, err := federationdomainproviders.NewFederationDomainIssuer( + federationDomain1.Spec.Issuer, + []*federationdomainproviders.FederationDomainIdentityProvider{}, + ) + require.NoError(t, err) - pinnipedInformerClient = pinnipedfake.NewSimpleClientset() - pinnipedInformers = pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0) - pinnipedAPIClient = pinnipedfake.NewSimpleClientset() + federationDomain2 := &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, + } - federationDomainGVR = schema.GroupVersionResource{ - Group: configv1alpha1.SchemeGroupVersion.Group, - Version: configv1alpha1.SchemeGroupVersion.Version, - Resource: "federationdomains", - } + federationDomain2Issuer, err := federationdomainproviders.NewFederationDomainIssuer( + federationDomain2.Spec.Issuer, + []*federationdomainproviders.FederationDomainIdentityProvider{}, + ) + require.NoError(t, err) - frozenMetav1Now = metav1.NewTime(frozenNow) - - happyReadyCondition = func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "Ready", - Status: "True", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "Success", - Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ - "the discovery endpoint is %s/.well-known/openid-configuration", issuer), - } - } + invalidFederationDomain := &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, + } - sadReadyCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "Ready", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "NotReady", - Message: "the FederationDomain is not ready: see other conditions for details", - } - } + happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "Ready", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ + "the discovery endpoint is %s/.well-known/openid-configuration", issuer), + } + } - happyIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerIsUnique", - Status: "True", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "Success", - Message: "spec.issuer is unique among all FederationDomains", - } - } + sadReadyCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "Ready", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + } + } - unknownIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerIsUnique", - Status: "Unknown", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "UnableToValidate", - Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", - } - } + happyIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "spec.issuer is unique among all FederationDomains", + } + } - sadIssuerIsUniqueCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerIsUnique", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "DuplicateIssuer", - Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", - } - } + unknownIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "Unknown", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "UnableToValidate", + Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", + } + } - happyOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "OneTLSSecretPerIssuerHostname", - Status: "True", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "Success", - Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", - } - } + sadIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerIsUnique", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DuplicateIssuer", + Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", + } + } - unknownOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "OneTLSSecretPerIssuerHostname", - Status: "Unknown", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "UnableToValidate", - 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", - } - } + happyOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", + } + } - sadOneTLSSecretPerIssuerHostnameCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "OneTLSSecretPerIssuerHostname", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "DifferentSecretRefsFound", - Message: "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found", - } - } + unknownOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "Unknown", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "UnableToValidate", + 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", + } + } - happyIssuerURLValidCondition = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerURLValid", - Status: "True", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "Success", - Message: "spec.issuer is a valid URL", - } - } + sadOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "OneTLSSecretPerIssuerHostname", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DifferentSecretRefsFound", + Message: "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found", + } + } - sadIssuerURLValidConditionCannotHaveQuery = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerURLValid", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "InvalidIssuerURL", - Message: "issuer must not have query", - } - } + happyIssuerURLValidCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "spec.issuer is a valid URL", + } + } - sadIssuerURLValidConditionCannotParse = func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "IssuerURLValid", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "InvalidIssuerURL", - Message: `could not parse issuer as URL: parse ":/host//path": missing protocol scheme`, - } - } + sadIssuerURLValidConditionCannotHaveQuery := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "InvalidIssuerURL", + Message: "issuer must not have query", + } + } - allHappyConditions = func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { - return []configv1alpha1.Condition{ - happyIssuerIsUniqueCondition(time, observedGeneration), - happyIssuerURLValidCondition(time, observedGeneration), - happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), - happyReadyCondition(issuer, time, observedGeneration), - } - } - }) + sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IssuerURLValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "InvalidIssuerURL", + Message: `could not parse issuer as URL: parse ":/host//path": missing protocol scheme`, + } + } - it.After(func() { - cancelContextCancelFunc() - }) + allHappyConditions := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + return []configv1alpha1.Condition{ + happyIssuerIsUniqueCondition(time, observedGeneration), + happyIssuerURLValidCondition(time, observedGeneration), + happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), + happyReadyCondition(issuer, time, observedGeneration), + } + } - when("there are some valid FederationDomains in the informer", func() { - var ( - federationDomain1 *configv1alpha1.FederationDomain - federationDomain2 *configv1alpha1.FederationDomain - ) + invalidIssuerURL := ":/host//path" + _, err = url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. + require.Error(t, err) + + newCopyWithStatus := func( + fd *configv1alpha1.FederationDomain, + phase configv1alpha1.FederationDomainPhase, + conditions []configv1alpha1.Condition, + ) *configv1alpha1.FederationDomain { + fdCopy := fd.DeepCopy() + fdCopy.Status.Phase = phase + fdCopy.Status.Conditions = conditions + return fdCopy + } - it.Before(func() { - federationDomain1 = &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, + tests := []struct { + name string + inputObjects []runtime.Object + configPinnipedClient func(*pinnipedfake.Clientset) + wantErr string + wantActions func(t *testing.T) []coretesting.Action + wantFederationDomainIssuers func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer + }{ + { + name: "there are no FederationDomains", + inputObjects: []runtime.Object{}, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{} + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{} + }, + }, + { + name: "there are some valid FederationDomains in the informer", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{ + federationDomain1Issuer, + federationDomain2Issuer, + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, + newCopyWithStatus(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + ), + ), + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + ), + ), } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain1)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain1)) - - federationDomain2 = &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, + }, + }, + { + name: "there are two valid FederationDomains, but one is already up to date, so only updates the out-of-date FederationDomain", + inputObjects: []runtime.Object{ + newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + ), + federationDomain2, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{ + federationDomain1Issuer, + federationDomain2Issuer, + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + ), + ), } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain2)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain2)) - }) - - it("calls the FederationDomainsSetter", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.ElementsMatch( - []*federationdomainproviders.FederationDomainIssuer{ - fd1, - fd2, + }, + }, + { + name: "there are two valid FederationDomains, but updating one fails", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor( + "update", + "federationdomains", + func(action coretesting.Action) (bool, runtime.Object, error) { + fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain) + if fd.Name == federationDomain1.Name { + return true, nil, errors.New("some update error") + } + return false, nil, nil }, - federationDomainsSetter.FederationDomainsReceived, ) - }) - - it("updates the status to ready in the FederationDomains", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - - federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain1.Namespace, - federationDomain1, + }, + wantErr: "could not update status: some update error", + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{ + federationDomain2Issuer, // federationDomain1 is not included because it encountered an error + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, + newCopyWithStatus(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain2.Namespace, - federationDomain2, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + ), ), } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - - when("one FederationDomain is already up to date", func() { - it.Before(func() { - federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - - r.NoError(pinnipedAPIClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) - r.NoError(pinnipedInformerClient.Tracker().Update(federationDomainGVR, federationDomain1, federationDomain1.Namespace)) - }) - - it("only updates the out-of-date FederationDomain", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain2.Namespace, - federationDomain2, - ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - - it("calls the FederationDomainsSetter with both FederationDomain's", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.ElementsMatch( - []*federationdomainproviders.FederationDomainIssuer{ - fd1, - fd2, - }, - federationDomainsSetter.FederationDomainsReceived, - ) - }) - }) - - when("updating only one FederationDomain fails", func() { - it.Before(func() { - once := sync.Once{} - pinnipedAPIClient.PrependReactor( - "update", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { - var err error - once.Do(func() { - err = errors.New("some update error") - }) - return true, nil, err - }, - ) - }) - - it("sets the FederationDomain that it could actually update in the API", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: some update error") - - fd1, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain1.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - fd2, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain2.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Len(federationDomainsSetter.FederationDomainsReceived, 1) - r.True( - reflect.DeepEqual(federationDomainsSetter.FederationDomainsReceived[0], fd1) || - reflect.DeepEqual(federationDomainsSetter.FederationDomainsReceived[0], fd2), - ) - }) - - it("returns an error", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: some update error") - - federationDomain1.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain1.Status.Conditions = allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123) - - federationDomain2.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain2.Status.Conditions = allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123) - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain1.Namespace, - federationDomain1, + }, + }, + { + name: "there are both valid and invalid FederationDomains", + inputObjects: []runtime.Object{ + invalidFederationDomain, + federationDomain2, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{ + federationDomain2Issuer, // only the valid FederationDomain + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus(invalidFederationDomain, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain2.Namespace, - federationDomain2, + ), + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - }) - - when("there are errors updating the FederationDomains", func() { - var ( - federationDomain *configv1alpha1.FederationDomain - ) - - it.Before(func() { - federationDomain = &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer.com"}, + ), } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) - }) - - when("updating the FederationDomain fails", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor( - "update", - "federationdomains", - func(_ coretesting.Action) (bool, runtime.Object, error) { + }, + }, + { + name: "there are both valid and invalid FederationDomains, but updating the invalid one fails", + inputObjects: []runtime.Object{ + invalidFederationDomain, + federationDomain2, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor( + "update", + "federationdomains", + func(action coretesting.Action) (bool, runtime.Object, error) { + fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain) + if fd.Name == invalidFederationDomain.Name { return true, nil, errors.New("some update error") - }, - ) - }) - - it("returns an error", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: some update error") - - federationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain.Namespace, - federationDomain, - ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - }) - - when("there are both valid and invalid FederationDomains in the informer", func() { - var ( - validFederationDomain *configv1alpha1.FederationDomain - invalidFederationDomain *configv1alpha1.FederationDomain - ) - - it.Before(func() { - validFederationDomain = &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "valid-config", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://valid-issuer.com"}, - } - r.NoError(pinnipedAPIClient.Tracker().Add(validFederationDomain)) - r.NoError(pinnipedInformerClient.Tracker().Add(validFederationDomain)) - - invalidFederationDomain = &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, - } - r.NoError(pinnipedAPIClient.Tracker().Add(invalidFederationDomain)) - r.NoError(pinnipedInformerClient.Tracker().Add(invalidFederationDomain)) - }) - - it("calls the FederationDomainsSetter with the valid FederationDomain", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - validFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Equal( - []*federationdomainproviders.FederationDomainIssuer{ - validFederationDomain, + } + return false, nil, nil }, - federationDomainsSetter.FederationDomainsReceived, ) - }) - - it("updates the status in each FederationDomain", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - validFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady - validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - - invalidFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseError - invalidFederationDomain.Status.Conditions = []configv1alpha1.Condition{ - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - invalidFederationDomain.Namespace, - invalidFederationDomain, + }, + wantErr: "could not update status: some update error", + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{ + federationDomain2Issuer, // only the valid FederationDomain + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus(invalidFederationDomain, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - validFederationDomain.Namespace, - validFederationDomain, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + ), ), } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - - when("updating only the invalid FederationDomain fails", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor( - "update", - "federationdomains", - func(action coretesting.Action) (bool, runtime.Object, error) { - updateAction := action.(coretesting.UpdateActionImpl) - federationDomain := updateAction.Object.(*configv1alpha1.FederationDomain) - if federationDomain.Name == validFederationDomain.Name { - return true, nil, nil - } - - return true, nil, errors.New("some update error") - }, - ) - }) - - it("sets the FederationDomain that it could actually update in the API", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: some update error") - - validFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(validFederationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Equal( - []*federationdomainproviders.FederationDomainIssuer{ - validFederationDomain, - }, - federationDomainsSetter.FederationDomainsReceived, - ) - }) - - it("returns an error", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.EqualError(err, "could not update status: some update error") - - validFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady - validFederationDomain.Status.Conditions = allHappyConditions(validFederationDomain.Spec.Issuer, frozenMetav1Now, 123) - - invalidFederationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseError - invalidFederationDomain.Status.Conditions = []configv1alpha1.Condition{ - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - invalidFederationDomain.Namespace, - invalidFederationDomain, - ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - validFederationDomain.Namespace, - validFederationDomain, - ), - } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - }) - - when("there are FederationDomains with duplicate issuer names in the informer", func() { - var ( - federationDomainDuplicate1 *configv1alpha1.FederationDomain - federationDomainDuplicate2 *configv1alpha1.FederationDomain - federationDomain *configv1alpha1.FederationDomain - ) - - it.Before(func() { - // Hostnames are case-insensitive, so consider them to be duplicates if they only differ by case. - // Paths are case-sensitive, so having a path that differs only by case makes a new issuer. - federationDomainDuplicate1 = &configv1alpha1.FederationDomain{ + }, + }, + { + name: "there are FederationDomains with duplicate issuer names", + inputObjects: []runtime.Object{ + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate1)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate1)) - federationDomainDuplicate2 = &configv1alpha1.FederationDomain{ + }, + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDuplicate2)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDuplicate2)) - - federationDomain = &configv1alpha1.FederationDomain{ + }, + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomain)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomain)) - }) - - it("calls the FederationDomainsSetter with the non-duplicate", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - nonDuplicateFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Equal( - []*federationdomainproviders.FederationDomainIssuer{ - nonDuplicateFederationDomain, - }, - federationDomainsSetter.FederationDomainsReceived, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) + }, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + fdi, err := federationdomainproviders.NewFederationDomainIssuer( + "https://issuer-duplicate.com/A", + []*federationdomainproviders.FederationDomainIdentityProvider{}, ) - }) - - it("updates the statuses", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - federationDomain.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomain.Status.Conditions = allHappyConditions(federationDomain.Spec.Issuer, frozenMetav1Now, 123) - - federationDomainDuplicate1.Status.Phase = configv1alpha1.FederationDomainPhaseError - federationDomainDuplicate1.Status.Conditions = []configv1alpha1.Condition{ - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - federationDomainDuplicate2.Status.Phase = configv1alpha1.FederationDomainPhaseError - federationDomainDuplicate2.Status.Conditions = []configv1alpha1.Condition{ - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainDuplicate1.Namespace, - federationDomainDuplicate1, + require.NoError(t, err) + return []*federationdomainproviders.FederationDomainIssuer{ + fdi, + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainDuplicate2.Namespace, - federationDomainDuplicate2, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomain.Namespace, - federationDomain, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions("https://issuer-duplicate.com/A", frozenMetav1Now, 123), + ), ), } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - - when("there are FederationDomains with the same issuer DNS hostname using different secretNames", func() { - var ( - federationDomainSameIssuerAddress1 *configv1alpha1.FederationDomain - federationDomainSameIssuerAddress2 *configv1alpha1.FederationDomain - federationDomainDifferentIssuerAddress *configv1alpha1.FederationDomain - federationDomainWithInvalidIssuerURL *configv1alpha1.FederationDomain - ) - - it.Before(func() { - federationDomainSameIssuerAddress1 = &configv1alpha1.FederationDomain{ + }, + }, + { + name: "there are FederationDomains with the same issuer DNS hostname using different secretNames", + inputObjects: []runtime.Object{ + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress1)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress1)) - federationDomainSameIssuerAddress2 = &configv1alpha1.FederationDomain{ + }, + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ // Validation treats these as the same DNS hostname even though they have different port numbers, @@ -830,124 +585,160 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Issuer: "https://issuer-duplicate-address.com:1234/path2", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, }, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainSameIssuerAddress2)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainSameIssuerAddress2)) - - federationDomainDifferentIssuerAddress = &configv1alpha1.FederationDomain{ + }, + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: "https://issuer-not-duplicate.com", TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainDifferentIssuerAddress)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainDifferentIssuerAddress)) - - // Also add one with a URL that cannot be parsed to make sure that the error handling - // for the duplicate issuers and secret names are not confused by invalid URLs. - invalidIssuerURL := ":/host//path" - _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. - r.Error(err) - federationDomainWithInvalidIssuerURL = &configv1alpha1.FederationDomain{ + }, + &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ Issuer: invalidIssuerURL, TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, - } - r.NoError(pinnipedAPIClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) - r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithInvalidIssuerURL)) - }) - - it("calls the FederationDomainsSetter with the non-duplicate", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - nonDuplicateFederationDomain, err := federationdomainproviders.NewFederationDomainIssuer(federationDomainDifferentIssuerAddress.Spec.Issuer, []*federationdomainproviders.FederationDomainIdentityProvider{}) - r.NoError(err) - - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Equal( - []*federationdomainproviders.FederationDomainIssuer{ - nonDuplicateFederationDomain, - }, - federationDomainsSetter.FederationDomainsReceived, + }, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + fdi, err := federationdomainproviders.NewFederationDomainIssuer( + "https://issuer-not-duplicate.com", + []*federationdomainproviders.FederationDomainIdentityProvider{}, ) - }) - - it("updates the statuses", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - - federationDomainDifferentIssuerAddress.Status.Phase = configv1alpha1.FederationDomainPhaseReady - federationDomainDifferentIssuerAddress.Status.Conditions = allHappyConditions(federationDomainDifferentIssuerAddress.Spec.Issuer, frozenMetav1Now, 123) - - federationDomainSameIssuerAddress1.Status.Phase = configv1alpha1.FederationDomainPhaseError - federationDomainSameIssuerAddress1.Status.Conditions = []configv1alpha1.Condition{ - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - federationDomainSameIssuerAddress2.Status.Phase = configv1alpha1.FederationDomainPhaseError - federationDomainSameIssuerAddress2.Status.Conditions = []configv1alpha1.Condition{ - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - federationDomainWithInvalidIssuerURL.Status.Phase = configv1alpha1.FederationDomainPhaseError - federationDomainWithInvalidIssuerURL.Status.Conditions = []configv1alpha1.Condition{ - unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), - unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - } - - expectedActions := []coretesting.Action{ - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainDifferentIssuerAddress.Namespace, - federationDomainDifferentIssuerAddress, + require.NoError(t, err) + return []*federationdomainproviders.FederationDomainIssuer{ + fdi, + } + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + }, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainSameIssuerAddress1.Namespace, - federationDomainSameIssuerAddress1, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer-duplicate-address.com:1234/path2", + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, + }, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainSameIssuerAddress2.Namespace, - federationDomainSameIssuerAddress2, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: invalidIssuerURL, + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + }, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), + unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), ), - coretesting.NewUpdateSubresourceAction( - federationDomainGVR, - "status", - federationDomainWithInvalidIssuerURL.Namespace, - federationDomainWithInvalidIssuerURL, + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer-not-duplicate.com", + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, + }, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditions("https://issuer-not-duplicate.com", frozenMetav1Now, 123), + ), ), } - r.ElementsMatch(expectedActions, pinnipedAPIClient.Actions()) - }) - }) + }, + }, + } - when("there are no FederationDomains in the informer", func() { - it("keeps waiting for one", func() { - startInformersAndController() - err := controllerlib.TestSync(t, subject, *syncContext) - r.NoError(err) - r.Empty(pinnipedAPIClient.Actions()) - r.True(federationDomainsSetter.SetFederationDomainsWasCalled) - r.Empty(federationDomainsSetter.FederationDomainsReceived) - }) + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + federationDomainsSetter := &fakeFederationDomainsSetter{} + pinnipedAPIClient := pinnipedfake.NewSimpleClientset() + pinnipedInformerClient := pinnipedfake.NewSimpleClientset() + for _, o := range tt.inputObjects { + require.NoError(t, pinnipedAPIClient.Tracker().Add(o)) + require.NoError(t, pinnipedInformerClient.Tracker().Add(o)) + } + if tt.configPinnipedClient != nil { + tt.configPinnipedClient(pinnipedAPIClient) + } + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0) + + controller := NewFederationDomainWatcherController( + federationDomainsSetter, + clocktesting.NewFakeClock(frozenNow), + pinnipedAPIClient, + pinnipedInformers.Config().V1alpha1().FederationDomains(), + pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), + controllerlib.WithInformer, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + pinnipedInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{Namespace: namespace, Name: "config-name"}} + + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + if tt.wantActions != nil { + require.ElementsMatch(t, tt.wantActions(t), pinnipedAPIClient.Actions()) + } else { + require.Empty(t, pinnipedAPIClient.Actions()) + } + + if tt.wantFederationDomainIssuers != nil { + require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) + require.ElementsMatch(t, tt.wantFederationDomainIssuers(t), federationDomainsSetter.FederationDomainsReceived) + } else { + require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) + } }) - }, spec.Parallel(), spec.Report(report.Terminal{})) + } } From e9fb4242d575eed061e382e5a9cbc42617b833b5 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Fri, 7 Jul 2023 17:10:07 -0400 Subject: [PATCH 29/81] Update federation_domain_watcher with new IdentityProviderFound - adds the truthy condition - TODOs for falsy conditions - addiional notes for other conditions - tests updated to pass with the new condition Co-authored-by: Ryan Richard --- .../federation_domain_watcher.go | 58 ++++- .../federation_domain_watcher_test.go | 203 +++++++++++++----- 2 files changed, 197 insertions(+), 64 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index c18b9fb13..c659470c0 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -35,13 +35,15 @@ const ( typeIssuerURLValid = "IssuerURLValid" typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" typeIssuerIsUnique = "IssuerIsUnique" + typeIdentityProvidersFound = "IdentityProvidersFound" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" - reasonInvalidIssuerURL = "InvalidIssuerURL" - reasonDuplicateIssuer = "DuplicateIssuer" - reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" + reasonSuccess = "Success" + reasonNotReady = "NotReady" + reasonUnableToValidate = "UnableToValidate" + reasonInvalidIssuerURL = "InvalidIssuerURL" + reasonDuplicateIssuer = "DuplicateIssuer" + reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" + reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess" celTransformerMaxExpressionRuntime = 5 * time.Second ) @@ -154,30 +156,57 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) if idpCRsCount == 1 { + foundIDPName := "" // If so, default that IDP's DisplayName to be the same as its resource Name. defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} switch { case len(oidcIdentityProviders) == 1: defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID + foundIDPName = oidcIdentityProviders[0].Name case len(ldapIdentityProviders) == 1: defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID + foundIDPName = ldapIdentityProviders[0].Name case len(activeDirectoryIdentityProviders) == 1: defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID + foundIDPName = activeDirectoryIdentityProviders[0].Name } // Backwards compatibility mode always uses an empty identity transformation pipline since no // transformations are defined on the FederationDomain. defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonLegacyConfigurationSuccess, + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ + "identity provider resource has been found: using %q as "+ + "identity provider: please explicitly list identity providers in .spec.identityProviders "+ + "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), + }) plog.Warning("detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication", "federationDomain", federationDomain.Name) } else { + // TODO(BEN): add the "falsy" status versions of this condition and add tests to cover. + // this matches the above "truthy" version of the condition above. + // // There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards // compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no // IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore // cannot actually be used to log in, but still serves endpoints. // TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it. + // write code for these two condition~ + // Type: IdentityProvidersFound + // Reason: LegacyConfigurationIdentityProviderNotFound + // Message: "no resources were specified by .spec.identityProviders[].objectRef and {number.of.idp.resources} identity provider resources have been found: please update .spec.identityProviders to specify which identity providers this federation domain should use" + // Status: false + // + // Type: IdentityProvidersFound + // Reason: LegacyConfigurationIdentityProviderNotFound + // Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource" + // status: false + // plog.Warning("FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled", "federationDomain", federationDomain.Name, "namespace", federationDomain.Namespace, @@ -354,6 +383,18 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro "identityProviderResourceUID", idpResourceUID, ) } + // TODO: + // - for the new "non-legacy" version of this to pass, we need to do this work. however, we don't need this for all of the tests for legacy functionality to work. + // Type: IdentityProvidersFound + // Reason: IdentityProvidersObjectRefsNotFound + // Message: ".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: {list.of.specific.resources.that.cant.be.found.in.case.some.can.be.found.but.not.all}" + // Status: false + // + // TODO: maaaaybe we need this happy case as well for the legacy functionality tests to fully pass? + // Type: IdentityProvidersFound + // Reason: Success + // Message: Non-legacy happy state "the resources specified by .spec.identityProviders[].objectRef were found" + // Status: true // Now that we have the list of IDPs for this FederationDomain, create the issuer. var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer @@ -392,7 +433,10 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) } } - + // BEN: notes: + // implementer of this function is the endpoint manager. + // it should re-setup all of the endpoints for the specified federation domains. + // (in test, this is wantFederationDomainIssues) c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) return errors.NewAggregate(errs) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 942f6c418..4e637a9ed 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/url" + "sort" "testing" "time" @@ -25,6 +26,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/testutil" ) @@ -129,33 +131,39 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Resource: "federationdomains", } + identityProvider := &idpv1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + UID: "some-uid", + }, + } + federationDomain1 := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, } - federationDomain1Issuer, err := federationdomainproviders.NewFederationDomainIssuer( - federationDomain1.Spec.Issuer, - []*federationdomainproviders.FederationDomainIdentityProvider{}, - ) - require.NoError(t, err) - federationDomain2 := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } - federationDomain2Issuer, err := federationdomainproviders.NewFederationDomainIssuer( - federationDomain2.Spec.Issuer, - []*federationdomainproviders.FederationDomainIdentityProvider{}, - ) - require.NoError(t, err) - invalidFederationDomain := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } + federationDomainIssuerWithDefaultIDP := func(t *testing.T, fedDomainIssuer string, idpObjectMeta metav1.ObjectMeta) *federationdomainproviders.FederationDomainIssuer { + fdIDP := &federationdomainproviders.FederationDomainIdentityProvider{ + DisplayName: idpObjectMeta.Name, + UID: idpObjectMeta.UID, + Transforms: idtransform.NewTransformationPipeline(), + } + fdIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(fedDomainIssuer, fdIDP) + require.NoError(t, err) + return fdIssuer + } + happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "Ready", @@ -278,8 +286,26 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - allHappyConditions := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersFound", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "LegacyConfigurationSuccess", + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ + "identity provider resource has been found: using %q as "+ + "identity provider: please explicitly list identity providers in .spec.identityProviders "+ + "(this legacy configuration mode may be removed in a future version of Pinniped)", idpName), + } + } + + // sadIdentityProvidersFoundConditionForSomeReasons := func() {} + + allHappyConditionsLegacyConfigurationSuccess := func(issuer, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ + // sorted alphabetically by type + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), happyIssuerIsUniqueCondition(time, observedGeneration), happyIssuerURLValidCondition(time, observedGeneration), happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), @@ -288,7 +314,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } invalidIssuerURL := ":/host//path" - _, err = url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. + _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. require.Error(t, err) newCopyWithStatus := func( @@ -311,7 +337,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantFederationDomainIssuers func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer }{ { - name: "there are no FederationDomains", + name: "when there are no FederationDomains, nothing happens", inputObjects: []runtime.Object{}, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{} @@ -321,15 +347,25 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, }, { - name: "there are some valid FederationDomains in the informer", + // TODO: fill in these conditions in my TODO blocks. + // conditions = append(conditions, &configv1alpha1.Condition{ + // Type: typeIssuerURLValid, + // Status: configv1alpha1.ConditionFalse, + // Reason: reasonInvalidIssuerURL, + // Message: err.Error(), + // }) + name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + + "matching the only identity provider found", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, + identityProvider, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomain1Issuer, - federationDomain2Issuer, + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -337,30 +373,32 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are two valid FederationDomains, but one is already up to date, so only updates the out-of-date FederationDomain", + name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + + "the out-of-date FederationDomain", inputObjects: []runtime.Object{ + identityProvider, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), federationDomain2, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomain1Issuer, - federationDomain2Issuer, + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -368,17 +406,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are two valid FederationDomains, but updating one fails", + name: "when there are two valid FederationDomains, but updating one fails, the status on the FederationDomain will not change", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, + identityProvider, }, configPinnipedClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( @@ -396,7 +435,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantErr: "could not update status: some update error", wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomain2Issuer, // federationDomain1 is not included because it encountered an error + // federationDomain1 is not included because it encountered an error + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -404,27 +444,30 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are both valid and invalid FederationDomains", + name: "when there are both valid and invalid FederationDomains, the status will be correctly set on each " + + "FederationDomain individually", inputObjects: []runtime.Object{ invalidFederationDomain, federationDomain2, + identityProvider, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomain2Issuer, // only the valid FederationDomain + // only the valid FederationDomain + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -433,6 +476,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { newCopyWithStatus(invalidFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -443,17 +487,19 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are both valid and invalid FederationDomains, but updating the invalid one fails", + name: "when there are both valid and invalid FederationDomains, but updating the invalid one fails, the " + + "existing status will be unchanged", inputObjects: []runtime.Object{ invalidFederationDomain, federationDomain2, + identityProvider, }, configPinnipedClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( @@ -471,7 +517,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantErr: "could not update status: some update error", wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomain2Issuer, // only the valid FederationDomain + // only the valid FederationDomain + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -480,6 +527,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { newCopyWithStatus(invalidFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -490,14 +538,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are FederationDomains with duplicate issuer names", + name: "when there are FederationDomains with duplicate issuer strings these particular FederationDomains " + + "will report error on IssuerUnique conditions", inputObjects: []runtime.Object{ &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, @@ -511,15 +560,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) }, + identityProvider, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - fdi, err := federationdomainproviders.NewFederationDomainIssuer( - "https://issuer-duplicate.com/A", - []*federationdomainproviders.FederationDomainIdentityProvider{}, - ) - require.NoError(t, err) return []*federationdomainproviders.FederationDomainIssuer{ - fdi, + // different path (paths are case-sensitive) + federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -532,6 +578,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -547,6 +594,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -561,14 +609,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions("https://issuer-duplicate.com/A", frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, { - name: "there are FederationDomains with the same issuer DNS hostname using different secretNames", + name: "when there are FederationDomains with the same issuer DNS hostname using different secretNames these " + + "particular FederationDomains will report errors on OneTLSSecretPerIssuerHostname conditions", inputObjects: []runtime.Object{ &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, @@ -600,15 +649,11 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, + identityProvider, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - fdi, err := federationdomainproviders.NewFederationDomainIssuer( - "https://issuer-not-duplicate.com", - []*federationdomainproviders.FederationDomainIdentityProvider{}, - ) - require.NoError(t, err) return []*federationdomainproviders.FederationDomainIssuer{ - fdi, + federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -624,6 +669,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -642,6 +688,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -660,6 +707,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -677,12 +725,30 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditions("https://issuer-not-duplicate.com", frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider.Name, frozenMetav1Now, 123), ), ), } }, }, + // TODO(Ben): add these additional tests to cover the new cases. There will likely also be more as we cover + // both the truthy as well as the falsy cases. + // { + // name: "legacy config: no identity provider specified in federation domain and no identity providers found", + // wantErr: "...please create an identity provider resource", + // }, + // { + // name: "legacy config: no identity provider specified in federation domain and multiple identity providers found", + // wantErr: "...to specify which identity providers this federation domain should use", + // }, + // { + // name: "the federation domain specifies identity providers that cannot be found", // single and/or multiple? + // wantErr: "...identifies resource(s) that cannot be found: {list.of...}", + // }, + // { + // name: "the federation domain specifies identity providers taht exist", + // wantErr: "", // n/a + // }, } for _, tt := range tests { @@ -727,18 +793,41 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { require.NoError(t, err) } - if tt.wantActions != nil { - require.ElementsMatch(t, tt.wantActions(t), pinnipedAPIClient.Actions()) - } else { - require.Empty(t, pinnipedAPIClient.Actions()) - } - if tt.wantFederationDomainIssuers != nil { require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) require.ElementsMatch(t, tt.wantFederationDomainIssuers(t), federationDomainsSetter.FederationDomainsReceived) } else { require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) } + + if tt.wantActions != nil { + // In this controller we don't actually care about the order of the actions, the FederationDomains + // can be updated in any order. Therefore, we are sorting here to make the test output easier to read. + // Unfortunately the timezone nested in the condition still makes it pretty ugly. + actualActions := pinnipedAPIClient.Actions() + sortActions(t, actualActions) + wantedActions := tt.wantActions(t) + sortActions(t, wantedActions) + require.Equal(t, wantedActions, actualActions) + } else { + require.Empty(t, pinnipedAPIClient.Actions()) + } }) } } + +func sortActions(t *testing.T, actions []coretesting.Action) { + sort.SliceStable(actions, func(prev, next int) bool { + updateAction1, ok := actions[prev].(coretesting.UpdateAction) + require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[prev]) + obj1, ok := updateAction1.GetObject().(metav1.Object) + require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[prev]) + + updateAction2, ok := actions[next].(coretesting.UpdateAction) + require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[next]) + obj2, ok := updateAction2.GetObject().(metav1.Object) + require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[next]) + + return obj1.GetName() < obj2.GetName() + }) +} From fe9364c58bf1e8c7d36826867c23bca77b8e0cb2 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 10 Jul 2023 17:09:19 -0400 Subject: [PATCH 30/81] Expand IdentityProvidersFound condition in federation_domain_watcher Co-authored-by: Ryan Richard --- .../federation_domain_watcher.go | 163 ++++++----- .../federation_domain_watcher_test.go | 273 ++++++++++++++---- 2 files changed, 304 insertions(+), 132 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index c659470c0..0f594c147 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -11,10 +11,11 @@ import ( "time" "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/errors" + errorsutil "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/clock" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -37,18 +38,21 @@ const ( typeIssuerIsUnique = "IssuerIsUnique" typeIdentityProvidersFound = "IdentityProvidersFound" - reasonSuccess = "Success" - reasonNotReady = "NotReady" - reasonUnableToValidate = "UnableToValidate" - reasonInvalidIssuerURL = "InvalidIssuerURL" - reasonDuplicateIssuer = "DuplicateIssuer" - reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" - reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess" + reasonSuccess = "Success" + reasonNotReady = "NotReady" + reasonUnableToValidate = "UnableToValidate" + reasonInvalidIssuerURL = "InvalidIssuerURL" + reasonDuplicateIssuer = "DuplicateIssuer" + reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" + reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess" + reasonLegacyConfigurationIdentityProviderNotFound = "LegacyConfigurationIdentityProviderNotFound" + reasonIdentityProvidersObjectRefsNotFound = "IdentityProvidersObjectRefsNotFound" + reasonIdentityProviderNotSpecified = "IdentityProviderNotSpecified" celTransformerMaxExpressionRuntime = 5 * time.Second ) -// FederationDomainsSetter can be notified of all known valid providers with its SetIssuer function. +// FederationDomainsSetter can be notified of all known valid providers with its SetFederationDomains function. // If there are no longer any valid issuers, then it can be called with no arguments. // Implementations of this type should be thread-safe to support calls from multiple goroutines. type FederationDomainsSetter interface { @@ -155,7 +159,9 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) - if idpCRsCount == 1 { + + switch { + case idpCRsCount == 1: foundIDPName := "" // If so, default that IDP's DisplayName to be the same as its resource Name. defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} @@ -173,7 +179,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID foundIDPName = activeDirectoryIdentityProviders[0].Name } - // Backwards compatibility mode always uses an empty identity transformation pipline since no + // Backwards compatibility mode always uses an empty identity transformation pipeline since no // transformations are defined on the FederationDomain. defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() conditions = append(conditions, &configv1alpha1.Condition{ @@ -185,42 +191,34 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro "identity provider: please explicitly list identity providers in .spec.identityProviders "+ "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), }) - plog.Warning("detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication", - "federationDomain", federationDomain.Name) - } else { - // TODO(BEN): add the "falsy" status versions of this condition and add tests to cover. - // this matches the above "truthy" version of the condition above. - // - // There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards - // compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no - // IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore - // cannot actually be used to log in, but still serves endpoints. - // TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it. - // write code for these two condition~ - // Type: IdentityProvidersFound - // Reason: LegacyConfigurationIdentityProviderNotFound - // Message: "no resources were specified by .spec.identityProviders[].objectRef and {number.of.idp.resources} identity provider resources have been found: please update .spec.identityProviders to specify which identity providers this federation domain should use" - // Status: false - // - // Type: IdentityProvidersFound - // Reason: LegacyConfigurationIdentityProviderNotFound - // Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource" - // status: false - // - plog.Warning("FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled", - "federationDomain", federationDomain.Name, - "namespace", federationDomain.Namespace, - "identityProvidersCustomResourcesCount", idpCRsCount, - ) + case idpCRsCount > 1: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ + "and %q identity provider resources have been found: "+ + "please update .spec.identityProviders to specify which identity providers "+ + "this federation domain should use", idpCRsCount), + }) + default: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonLegacyConfigurationIdentityProviderNotFound, + Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + + "resources have been found: please create an identity provider resource", + }) } } // If there is an explicit list of IDPs on the FederationDomain, then process the list. celTransformer, _ := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? - // TODO: handle err - for _, idp := range federationDomain.Spec.IdentityProviders { + // TODO: handle err from NewCELTransformer() above + + idpNotFoundIndices := []int{} + for index, idp := range federationDomain.Spec.IdentityProviders { var idpResourceUID types.UID - var idpResourceName string // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself @@ -228,29 +226,35 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // that does not resolve, put an error on the FederationDomain status. switch idp.ObjectRef.Kind { case "LDAPIdentityProvider": - ldapIDP, _ := c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - // TODO: handle notfound err and also unexpected errors - idpResourceName = ldapIDP.Name - idpResourceUID = ldapIDP.UID + ldapIDP, err := c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + if err == nil { + idpResourceUID = ldapIDP.UID + } else if errors.IsNotFound(err) { + idpNotFoundIndices = append(idpNotFoundIndices, index) + } else { + // TODO: handle unexpected errors + } case "ActiveDirectoryIdentityProvider": - adIDP, _ := c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - // TODO: handle notfound err and also unexpected errors - idpResourceName = adIDP.Name - idpResourceUID = adIDP.UID + adIDP, err := c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + if err == nil { + idpResourceUID = adIDP.UID + } else if errors.IsNotFound(err) { + idpNotFoundIndices = append(idpNotFoundIndices, index) + } else { + // TODO: handle unexpected errors + } case "OIDCIdentityProvider": - oidcIDP, _ := c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - // TODO: handle notfound err and also unexpected errors - idpResourceName = oidcIDP.Name - idpResourceUID = oidcIDP.UID + oidcIDP, err := c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) + if err == nil { + idpResourceUID = oidcIDP.UID + } else if errors.IsNotFound(err) { + idpNotFoundIndices = append(idpNotFoundIndices, index) + } else { + // TODO: handle unexpected errors + } default: - // TODO: handle bad user input + // TODO: handle an IDP type that we do not understand. } - plog.Debug("resolved identity provider object reference", - "kind", idp.ObjectRef.Kind, - "name", idp.ObjectRef.Name, - "foundResourceName", idpResourceName, - "foundResourceUID", idpResourceUID, - ) // Prepare the transformations. pipeline := idtransform.NewTransformationPipeline() @@ -383,18 +387,31 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro "identityProviderResourceUID", idpResourceUID, ) } - // TODO: - // - for the new "non-legacy" version of this to pass, we need to do this work. however, we don't need this for all of the tests for legacy functionality to work. - // Type: IdentityProvidersFound - // Reason: IdentityProvidersObjectRefsNotFound - // Message: ".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: {list.of.specific.resources.that.cant.be.found.in.case.some.can.be.found.but.not.all}" - // Status: false - // - // TODO: maaaaybe we need this happy case as well for the legacy functionality tests to fully pass? - // Type: IdentityProvidersFound - // Reason: Success - // Message: Non-legacy happy state "the resources specified by .spec.identityProviders[].objectRef were found" - // Status: true + + if len(idpNotFoundIndices) != 0 { + msgs := []string{} + for _, idpIndex := range idpNotFoundIndices { + idp := federationDomain.Spec.IdentityProviders[idpIndex] + displayName := idp.DisplayName + msgs = append(msgs, fmt.Sprintf("IDP with displayName %q at index %d", displayName, idpIndex)) + } + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", strings.Join(msgs, ", ")), + }) + } else { + // TODO: write tests for this case. + // if len(federationDomain.Spec.IdentityProviders) != 0 { + // conditions = append(conditions, &configv1alpha1.Condition{ + // Type: typeIdentityProvidersFound, + // Status: configv1alpha1.ConditionTrue, + // Reason: reasonSuccess, + // Message: "the resources specified by .spec.identityProviders[].objectRef were found", + // }) + // } + } // Now that we have the list of IDPs for this FederationDomain, create the issuer. var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer @@ -439,7 +456,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro // (in test, this is wantFederationDomainIssues) c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) - return errors.NewAggregate(errs) + return errorsutil.NewAggregate(errs) } func (c *federationDomainWatcherController) updateStatus( diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 4e637a9ed..e80854a94 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -19,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" + "k8s.io/utils/pointer" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -121,6 +122,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { t.Parallel() const namespace = "some-namespace" + const apiGroupSupervisor = "idp.supervisor.pinniped.dev" frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenMetav1Now := metav1.NewTime(frozenNow) @@ -131,10 +133,16 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Resource: "federationdomains", } - identityProvider := &idpv1alpha1.OIDCIdentityProvider{ + identityProvider1 := &idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-name", - UID: "some-uid", + Name: "some-name1", + UID: "some-uid1", + }, + } + identityProvider2 := &idpv1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name2", + UID: "some-uid2", }, } @@ -300,7 +308,42 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - // sadIdentityProvidersFoundConditionForSomeReasons := func() {} + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersFound", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "LegacyConfigurationIdentityProviderNotFound", + Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + + "resources have been found: please create an identity provider resource", + } + } + + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified := func(idpCRsCount int, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersFound", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "IdentityProviderNotSpecified", + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ + "and %q identity provider resources have been found: "+ + "please update .spec.identityProviders to specify which identity providers "+ + "this federation domain should use", idpCRsCount), + } + } + + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(msg string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersFound", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "IdentityProvidersObjectRefsNotFound", + Message: msg, + } + } allHappyConditionsLegacyConfigurationSuccess := func(issuer, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ @@ -347,25 +390,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, }, { - // TODO: fill in these conditions in my TODO blocks. - // conditions = append(conditions, &configv1alpha1.Condition{ - // Type: typeIssuerURLValid, - // Status: configv1alpha1.ConditionFalse, - // Reason: reasonInvalidIssuerURL, - // Message: err.Error(), - // }) name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + "matching the only identity provider found", inputObjects: []runtime.Object{ federationDomain1, federationDomain2, - identityProvider, + identityProvider1, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -373,13 +409,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -389,16 +425,16 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + "the out-of-date FederationDomain", inputObjects: []runtime.Object{ - identityProvider, + identityProvider1, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), federationDomain2, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -406,7 +442,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -417,7 +453,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ federationDomain1, federationDomain2, - identityProvider, + identityProvider1, }, configPinnipedClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( @@ -436,7 +472,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ // federationDomain1 is not included because it encountered an error - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -444,13 +480,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -462,12 +498,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ invalidFederationDomain, federationDomain2, - identityProvider, + identityProvider1, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -476,7 +512,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { newCopyWithStatus(invalidFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -487,7 +523,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -499,7 +535,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ invalidFederationDomain, federationDomain2, - identityProvider, + identityProvider1, }, configPinnipedClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( @@ -518,7 +554,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -527,7 +563,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { newCopyWithStatus(invalidFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -538,7 +574,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, newCopyWithStatus(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -560,12 +596,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) }, - identityProvider, + identityProvider1, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ // different path (paths are case-sensitive) - federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -578,7 +614,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -594,7 +630,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -609,7 +645,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider1.Name, frozenMetav1Now, 123), ), ), } @@ -649,11 +685,11 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, - identityProvider, + identityProvider1, }, wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider1.ObjectMeta), } }, wantActions: func(t *testing.T) []coretesting.Action { @@ -669,7 +705,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -688,7 +724,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -707,7 +743,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -725,28 +761,147 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider1.Name, frozenMetav1Now, 123), + ), + ), + } + }, + }, + { + name: "legacy config: no identity provider specified in federation domain and no identity providers found results in not found status", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{} + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, + newCopyWithStatus(federationDomain1, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + ), + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, + newCopyWithStatus(federationDomain2, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + ), + } + }, + }, + { + name: "legacy config: no identity provider specified in federation domain and multiple identity providers found results in not specified status", + inputObjects: []runtime.Object{ + federationDomain1, + identityProvider1, + identityProvider2, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{} + }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, + newCopyWithStatus(federationDomain1, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(2, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + ), + } + }, + }, + { + name: "the federation domain specifies identity providers that cannot be found", + inputObjects: []runtime.Object{ + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "cant-find-me", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: "cant-find-me", + }, + }, + }, + }, + }, + }, + wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { + return []*federationdomainproviders.FederationDomainIssuer{} + }, + // TODO: sketch out a way to eliminate the whole wantActions and be more precise + // if not wantActions, might wantStatusUpdate? + // wantStatusUpdate: func(t *testing.T, []configv1alpha1.FederationDomain) { + // { + // Name: "foo", + // Namespace: "bar", + // Phase: "Happy", + // Conditions: []metav1.Condition{}, + // } + // }, + wantActions: func(t *testing.T) []coretesting.Action { + return []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", namespace, + newCopyWithStatus( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "cant-find-me", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: "cant-find-me", + }, + }, + }, + }, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ + `IDP with displayName "cant-find-me" at index 0`, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, ), ), } }, }, - // TODO(Ben): add these additional tests to cover the new cases. There will likely also be more as we cover - // both the truthy as well as the falsy cases. - // { - // name: "legacy config: no identity provider specified in federation domain and no identity providers found", - // wantErr: "...please create an identity provider resource", - // }, - // { - // name: "legacy config: no identity provider specified in federation domain and multiple identity providers found", - // wantErr: "...to specify which identity providers this federation domain should use", - // }, - // { - // name: "the federation domain specifies identity providers that cannot be found", // single and/or multiple? - // wantErr: "...identifies resource(s) that cannot be found: {list.of...}", - // }, // { - // name: "the federation domain specifies identity providers taht exist", + // name: "the federation domain specifies identity providers that exist", // wantErr: "", // n/a // }, } From 97a374c00b2302f77130b0799a4e562a207b40db Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 10 Jul 2023 15:41:48 -0700 Subject: [PATCH 31/81] Refactor federation_domain_watcher_test.go and add new test to its table --- .../federation_domain_watcher.go | 22 +- .../federation_domain_watcher_test.go | 783 +++++++++--------- 2 files changed, 396 insertions(+), 409 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 0f594c147..60d196e43 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -402,15 +402,14 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", strings.Join(msgs, ", ")), }) } else { - // TODO: write tests for this case. - // if len(federationDomain.Spec.IdentityProviders) != 0 { - // conditions = append(conditions, &configv1alpha1.Condition{ - // Type: typeIdentityProvidersFound, - // Status: configv1alpha1.ConditionTrue, - // Reason: reasonSuccess, - // Message: "the resources specified by .spec.identityProviders[].objectRef were found", - // }) - // } + if len(federationDomain.Spec.IdentityProviders) != 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + }) + } } // Now that we have the list of IDPs for this FederationDomain, create the issuer. @@ -450,10 +449,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) } } - // BEN: notes: - // implementer of this function is the endpoint manager. - // it should re-setup all of the endpoints for the specified federation domains. - // (in test, this is wantFederationDomainIssues) + c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) return errorsutil.NewAggregate(errs) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index e80854a94..459e2ee10 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -118,6 +118,12 @@ func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ... f.FederationDomainsReceived = federationDomains } +var federationDomainGVR = schema.GroupVersionResource{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Resource: "federationdomains", +} + func TestTestFederationDomainWatcherControllerSync(t *testing.T) { t.Parallel() @@ -127,22 +133,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenMetav1Now := metav1.NewTime(frozenNow) - federationDomainGVR := schema.GroupVersionResource{ - Group: configv1alpha1.SchemeGroupVersion.Group, - Version: configv1alpha1.SchemeGroupVersion.Version, - Resource: "federationdomains", - } - identityProvider1 := &idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-name1", - UID: "some-uid1", + Name: "some-name1", + Namespace: namespace, + UID: "some-uid1", }, } identityProvider2 := &idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-name2", - UID: "some-uid2", + Name: "some-name2", + Namespace: namespace, + UID: "some-uid2", }, } @@ -156,11 +158,17 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, } - invalidFederationDomain := &configv1alpha1.FederationDomain{ + invalidIssuerURLFederationDomain := &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, } + federationDomainIssuerWithIDPs := func(t *testing.T, fedDomainIssuer string, fdIDPs []*federationdomainproviders.FederationDomainIdentityProvider) *federationdomainproviders.FederationDomainIssuer { + fdIssuer, err := federationdomainproviders.NewFederationDomainIssuer(fedDomainIssuer, fdIDPs) + require.NoError(t, err) + return fdIssuer + } + federationDomainIssuerWithDefaultIDP := func(t *testing.T, fedDomainIssuer string, idpObjectMeta metav1.ObjectMeta) *federationdomainproviders.FederationDomainIssuer { fdIDP := &federationdomainproviders.FederationDomainIdentityProvider{ DisplayName: idpObjectMeta.Name, @@ -308,6 +316,17 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyIdentityProvidersFoundConditionSuccess := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersFound", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + } + } + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", @@ -345,9 +364,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - allHappyConditionsLegacyConfigurationSuccess := func(issuer, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ - // sorted alphabetically by type + // expect them to be sorted alphabetically by type happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), happyIssuerIsUniqueCondition(time, observedGeneration), happyIssuerURLValidCondition(time, observedGeneration), @@ -356,38 +375,33 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + return []configv1alpha1.Condition{ + // expect them to be sorted alphabetically by type + happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + happyReadyCondition(issuer, frozenMetav1Now, 123), + } + } + invalidIssuerURL := ":/host//path" _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. require.Error(t, err) - newCopyWithStatus := func( - fd *configv1alpha1.FederationDomain, - phase configv1alpha1.FederationDomainPhase, - conditions []configv1alpha1.Condition, - ) *configv1alpha1.FederationDomain { - fdCopy := fd.DeepCopy() - fdCopy.Status.Phase = phase - fdCopy.Status.Conditions = conditions - return fdCopy - } - tests := []struct { name string inputObjects []runtime.Object configPinnipedClient func(*pinnipedfake.Clientset) wantErr string - wantActions func(t *testing.T) []coretesting.Action - wantFederationDomainIssuers func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer + wantStatusUpdates []*configv1alpha1.FederationDomain + wantFederationDomainIssuers []*federationdomainproviders.FederationDomainIssuer }{ { - name: "when there are no FederationDomains, nothing happens", - inputObjects: []runtime.Object{}, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{} - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{} - }, + name: "when there are no FederationDomains, no update actions happen and the list of FederationDomainIssuers is set to the empty list", + inputObjects: []runtime.Object{}, + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, }, { name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + @@ -398,27 +412,19 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { federationDomain2, identityProvider1, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, - newCopyWithStatus(federationDomain1, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { @@ -426,26 +432,26 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { "the out-of-date FederationDomain", inputObjects: []runtime.Object{ identityProvider1, - newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer}, + Status: configv1alpha1.FederationDomainStatus{ + Phase: configv1alpha1.FederationDomainPhaseReady, + Conditions: allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + }, + }, federationDomain2, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + // only one update, because the other FederationDomain already had the right status + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { @@ -469,71 +475,55 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ) }, wantErr: "could not update status: some update error", - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - // federationDomain1 is not included because it encountered an error - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, - newCopyWithStatus(federationDomain1, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + // federationDomain1 is not included because it encountered an error + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { name: "when there are both valid and invalid FederationDomains, the status will be correctly set on each " + "FederationDomain individually", inputObjects: []runtime.Object{ - invalidFederationDomain, + invalidIssuerURLFederationDomain, federationDomain2, identityProvider1, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus(invalidFederationDomain, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + // only the valid FederationDomain + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { name: "when there are both valid and invalid FederationDomains, but updating the invalid one fails, the " + "existing status will be unchanged", inputObjects: []runtime.Object{ - invalidFederationDomain, + invalidIssuerURLFederationDomain, federationDomain2, identityProvider1, }, @@ -543,7 +533,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { "federationdomains", func(action coretesting.Action) (bool, runtime.Object, error) { fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain) - if fd.Name == invalidFederationDomain.Name { + if fd.Name == invalidIssuerURLFederationDomain.Name { return true, nil, errors.New("some update error") } return false, nil, nil @@ -551,33 +541,25 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ) }, wantErr: "could not update status: some update error", - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus(invalidFederationDomain, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + // only the valid FederationDomain + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { @@ -598,57 +580,43 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, identityProvider1, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - // different path (paths are case-sensitive) - federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"}, - }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"}, - }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, - }, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { @@ -687,84 +655,56 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, identityProvider1, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider1.ObjectMeta), - } - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1", - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, - }, - }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: "https://issuer-duplicate-address.com:1234/path2", - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"}, - }, - }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", invalidFederationDomain.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: invalidIssuerURL, - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, - }, - }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), - unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), - unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: "https://issuer-not-duplicate.com", - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, - }, - }, - configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider1.Name, frozenMetav1Now, 123), - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider1.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), + unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider1.Name, frozenMetav1Now, 123), + ), }, }, { @@ -773,36 +713,28 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { federationDomain1, federationDomain2, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{} - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, - newCopyWithStatus(federationDomain1, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, - newCopyWithStatus(federationDomain2, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), }, }, { @@ -812,24 +744,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { identityProvider1, identityProvider2, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{} - }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, - newCopyWithStatus(federationDomain1, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(2, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }, - ), - ), - } + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(2, frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), }, }, { @@ -848,62 +774,93 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: "cant-find-me", }, }, + { + DisplayName: "cant-find-me-either", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: "cant-find-me-either", + }, + }, }, }, }, }, - wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { - return []*federationdomainproviders.FederationDomainIssuer{} - }, - // TODO: sketch out a way to eliminate the whole wantActions and be more precise - // if not wantActions, might wantStatusUpdate? - // wantStatusUpdate: func(t *testing.T, []configv1alpha1.FederationDomain) { - // { - // Name: "foo", - // Namespace: "bar", - // Phase: "Happy", - // Conditions: []metav1.Condition{}, - // } - // }, - wantActions: func(t *testing.T) []coretesting.Action { - return []coretesting.Action{ - coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", namespace, - newCopyWithStatus( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: "https://issuer1.com", - IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ - { - DisplayName: "cant-find-me", - ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), - Kind: "OIDCIdentityProvider", - Name: "cant-find-me", - }, - }, - }, + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ + `IDP with displayName "cant-find-me" at index 0, IDP with displayName "cant-find-me-either" at index 1`, + frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }, + ), + }, + }, + { + name: "the federation domain specifies identity providers that all exist", + inputObjects: []runtime.Object{ + identityProvider1, + identityProvider2, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "can-find-me", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: identityProvider1.Name, }, }, - configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ - `IDP with displayName "cant-find-me" at index 0`, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), + { + DisplayName: "can-find-me-too", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: identityProvider2.Name, + }, }, - ), - ), - } + }, + }, + }, + }, + wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithIDPs(t, "https://issuer1.com", + []*federationdomainproviders.FederationDomainIdentityProvider{ + { + DisplayName: "can-find-me", + UID: identityProvider1.UID, + Transforms: idtransform.NewTransformationPipeline(), + }, + { + DisplayName: "can-find-me-too", + UID: identityProvider2.UID, + Transforms: idtransform.NewTransformationPipeline(), + }, + }), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + ), }, }, - // { - // name: "the federation domain specifies identity providers that exist", - // wantErr: "", // n/a - // }, } for _, tt := range tests { @@ -950,20 +907,23 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { if tt.wantFederationDomainIssuers != nil { require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) - require.ElementsMatch(t, tt.wantFederationDomainIssuers(t), federationDomainsSetter.FederationDomainsReceived) + require.ElementsMatch(t, tt.wantFederationDomainIssuers, federationDomainsSetter.FederationDomainsReceived) } else { require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) } - if tt.wantActions != nil { - // In this controller we don't actually care about the order of the actions, the FederationDomains - // can be updated in any order. Therefore, we are sorting here to make the test output easier to read. - // Unfortunately the timezone nested in the condition still makes it pretty ugly. - actualActions := pinnipedAPIClient.Actions() - sortActions(t, actualActions) - wantedActions := tt.wantActions(t) - sortActions(t, wantedActions) - require.Equal(t, wantedActions, actualActions) + if tt.wantStatusUpdates != nil { + // This controller should only perform updates to FederationDomain statuses. + // In this controller we don't actually care about the order of the actions, since the FederationDomains + // statuses can be updated in any order. Therefore, we are sorting here so we can use require.Equal + // to make the test output easier to read. Unfortunately the timezone nested in the condition can still + // make the test failure diffs ugly sometimes, but we do want to assert about timestamps so there's not + // much we can do about those. + actualFederationDomainUpdates := getFederationDomainStatusUpdates(t, pinnipedAPIClient.Actions()) + sortFederationDomainsByName(actualFederationDomainUpdates) + sortFederationDomainsByName(tt.wantStatusUpdates) + // Use require.Equal instead of require.ElementsMatch because require.Equal prints a nice diff. + require.Equal(t, tt.wantStatusUpdates, actualFederationDomainUpdates) } else { require.Empty(t, pinnipedAPIClient.Actions()) } @@ -971,18 +931,49 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } -func sortActions(t *testing.T, actions []coretesting.Action) { - sort.SliceStable(actions, func(prev, next int) bool { - updateAction1, ok := actions[prev].(coretesting.UpdateAction) - require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[prev]) - obj1, ok := updateAction1.GetObject().(metav1.Object) - require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[prev]) +func expectedFederationDomainStatusUpdate( + fd *configv1alpha1.FederationDomain, + phase configv1alpha1.FederationDomainPhase, + conditions []configv1alpha1.Condition, +) *configv1alpha1.FederationDomain { + fdCopy := fd.DeepCopy() + + // We don't care about the spec of a FederationDomain in an update status action, + // so clear it out to make it easier to write expected values. + fdCopy.Spec = configv1alpha1.FederationDomainSpec{} - updateAction2, ok := actions[next].(coretesting.UpdateAction) - require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[next]) - obj2, ok := updateAction2.GetObject().(metav1.Object) - require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[next]) + fdCopy.Status.Phase = phase + fdCopy.Status.Conditions = conditions + + return fdCopy +} + +func getFederationDomainStatusUpdates(t *testing.T, actions []coretesting.Action) []*configv1alpha1.FederationDomain { + federationDomains := []*configv1alpha1.FederationDomain{} + + for _, action := range actions { + updateAction, ok := action.(coretesting.UpdateAction) + require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction: %#v", action) + require.Equal(t, federationDomainGVR, updateAction.GetResource(), "an update action should have updated a FederationDomain but updated something else") + require.Equal(t, "status", updateAction.GetSubresource(), "an update action should have updated the status subresource but updated something else") + + fd, ok := updateAction.GetObject().(*configv1alpha1.FederationDomain) + require.True(t, ok, "failed to cast an action's object as a FederationDomain: %#v", updateAction.GetObject()) + require.Equal(t, fd.Namespace, updateAction.GetNamespace(), "an update action might have been called on the wrong namespace for a FederationDomain") + + // We don't care about the spec of a FederationDomain in an update status action, + // so clear it out to make it easier to write expected values. + copyOfFD := fd.DeepCopy() + copyOfFD.Spec = configv1alpha1.FederationDomainSpec{} + + federationDomains = append(federationDomains, copyOfFD) + } + + return federationDomains +} - return obj1.GetName() < obj2.GetName() +func sortFederationDomainsByName(federationDomains []*configv1alpha1.FederationDomain) { + sort.SliceStable(federationDomains, func(a, b int) bool { + return federationDomains[a].GetName() < federationDomains[b].GetName() }) } From 40dcc8a7f1d1a95d0ee16d977efc4761379200d2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 10 Jul 2023 17:23:27 -0700 Subject: [PATCH 32/81] Update integration tests for new FederationDomain phase behavior - Refactor testlib.CreateTestFederationDomain helper - Call testlib.WaitForTestFederationDomainStatus after each integration test creates an IDP and expects the FederationDomain to become ready - Create an IDP for some tests which want the FederationDomain to be ready but were previously not creating any IDP - Expect the new FederationDomain condition type "IdentityProvidersFound" in those tests where it is needed Co-authored-by: Joshua Casey --- test/integration/e2e_test.go | 22 ++++++- test/integration/supervisor_discovery_test.go | 61 +++++++++++++++---- test/integration/supervisor_login_test.go | 22 ++++--- test/integration/supervisor_secrets_test.go | 8 ++- test/integration/supervisor_warnings_test.go | 11 +++- test/testlib/client.go | 21 +------ 6 files changed, 99 insertions(+), 46 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 5e8c800ba..09bf7e315 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -101,9 +101,11 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Create the downstream FederationDomain and expect it to go into the success status condition. downstream := testlib.CreateTestFederationDomain(topSetupCtx, t, - issuerURL.String(), - certSecret.Name, - configv1alpha1.FederationDomainPhaseReady, + configv1alpha1.FederationDomainSpec{ + Issuer: issuerURL.String(), + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + }, + configv1alpha1.FederationDomainPhaseError, // in phase error until there is an IDP created ) // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. @@ -156,6 +158,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -238,6 +241,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -322,6 +326,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -442,6 +447,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -569,6 +575,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -638,6 +645,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -710,6 +718,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -765,6 +774,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -824,6 +834,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -891,6 +902,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -946,6 +958,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1015,6 +1028,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1066,6 +1080,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1117,6 +1132,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index d591c139c..c971584a3 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/util/retry" "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/crypto/ptls" @@ -82,6 +83,12 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { t.Skip("no address defined") } + // Create any IDP so that any FederationDomain created later by this test will see that exactly one IDP exists. + testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + // Test that there is no default discovery endpoint available when there are no FederationDomains. requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, fmt.Sprintf("%s://%s", scheme, addr)) @@ -124,16 +131,18 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving. config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) - config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, issuer6, "", "") + config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuer6}, v1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, "IssuerIsUnique": v1alpha1.ConditionFalse, + "IdentityProvidersFound": v1alpha1.ConditionTrue, "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, "IssuerURLValid": v1alpha1.ConditionTrue, }) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, "IssuerIsUnique": v1alpha1.ConditionFalse, + "IdentityProvidersFound": v1alpha1.ConditionTrue, "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, "IssuerURLValid": v1alpha1.ConditionTrue, }) @@ -153,10 +162,11 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config7, client, ns, scheme, addr, caBundle, issuer7) // When we create a provider with an invalid issuer, the status is set to invalid. - badConfig := testlib.CreateTestFederationDomain(ctx, t, badIssuer, "", "") + badConfig := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: badIssuer}, v1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, badConfig.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, "IssuerIsUnique": v1alpha1.ConditionTrue, + "IdentityProvidersFound": v1alpha1.ConditionTrue, "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, "IssuerURLValid": v1alpha1.ConditionFalse, }) @@ -176,6 +186,12 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() + // Create any IDP so that any FederationDomain created later by this test will see that exactly one IDP exists. + testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret(ctx, t, ns, defaultTLSCertSecretName(env), pinnipedClient, kubeClient) scheme := "https" @@ -186,7 +202,11 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { certSecretName1 := "integration-test-cert-1" // Create an FederationDomain with a spec.tls.secretName. - federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, issuer1, certSecretName1, "") + federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, + v1alpha1.FederationDomainSpec{ + Issuer: issuer1, + TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName1}, + }, v1alpha1.FederationDomainPhaseReady) requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) // The spec.tls.secretName Secret does not exist, so the endpoints should fail with TLS errors. @@ -210,7 +230,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { return err })) - // The the endpoints should fail with TLS errors again. + // The endpoints should fail with TLS errors again. requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady(t, issuer1) // Create a Secret at the updated name. @@ -226,7 +246,11 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { certSecretName2 := "integration-test-cert-2" // Create an FederationDomain with a spec.tls.secretName. - federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, issuer2, certSecretName2, "") + federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, + v1alpha1.FederationDomainSpec{ + Issuer: issuer2, + TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName2}, + }, v1alpha1.FederationDomainPhaseReady) requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) // Create the Secret. @@ -248,6 +272,12 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() + // Create any IDP so that any FederationDomain created later by this test will see that exactly one IDP exists. + testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret(ctx, t, ns, defaultTLSCertSecretName(env), pinnipedClient, kubeClient) scheme := "https" @@ -270,7 +300,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { issuerUsingHostname := fmt.Sprintf("%s://%s/issuer1", scheme, address) // Create an FederationDomain without a spec.tls.secretName. - federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, issuerUsingIPAddress, "", "") + federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuerUsingIPAddress}, v1alpha1.FederationDomainPhaseReady) requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) // There is no default TLS cert and the spec.tls.secretName was not set, so the endpoints should fail with TLS errors. @@ -284,7 +314,11 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { // Create an FederationDomain with a spec.tls.secretName. certSecretName := "integration-test-cert-1" - federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, issuerUsingHostname, certSecretName, "") + federationDomain2 := testlib.CreateTestFederationDomain(ctx, t, + v1alpha1.FederationDomainSpec{ + Issuer: issuerUsingHostname, + TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName}, + }, v1alpha1.FederationDomainPhaseReady) requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) // Create the Secret. @@ -471,7 +505,7 @@ func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear( client pinnipedclientset.Interface, ) (*v1alpha1.FederationDomain, *ExpectedJWKSResponseFormat) { t.Helper() - newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, issuerName, "", "") + newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuerName}, v1alpha1.FederationDomainPhaseReady) jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) requireFullySuccessfulStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name) return newFederationDomain, jwksResult @@ -645,12 +679,13 @@ func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interfa requireStatus(t, client, ns, name, v1alpha1.FederationDomainPhaseReady, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionTrue, "IssuerIsUnique": v1alpha1.ConditionTrue, + "IdentityProvidersFound": v1alpha1.ConditionTrue, "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, "IssuerURLValid": v1alpha1.ConditionTrue, }) } -func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, phase v1alpha1.FederationDomainPhase, conditionTypeToStatus map[string]v1alpha1.ConditionStatus) { +func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, wantPhase v1alpha1.FederationDomainPhase, wantConditionTypeToStatus map[string]v1alpha1.ConditionStatus) { t.Helper() testlib.RequireEventually(t, func(requireEventually *require.Assertions) { @@ -660,14 +695,16 @@ func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name st federationDomain, err := client.ConfigV1alpha1().FederationDomains(ns).Get(ctx, name, metav1.GetOptions{}) requireEventually.NoError(err) - t.Logf("found FederationDomain %s/%s with phase %s", ns, name, federationDomain.Status.Phase) - requireEventually.Equalf(phase, federationDomain.Status.Phase, "unexpected phase (conditions = '%#v')", federationDomain.Status.Conditions) + actualPhase := federationDomain.Status.Phase + t.Logf("found FederationDomain %s/%s with phase %s, wanted phase %s", ns, name, actualPhase, wantPhase) + requireEventually.Equalf(wantPhase, actualPhase, "unexpected phase (conditions = '%#v')", federationDomain.Status.Conditions) actualConditionTypeToStatus := map[string]v1alpha1.ConditionStatus{} for _, c := range federationDomain.Status.Conditions { actualConditionTypeToStatus[c.Type] = c.Status } - requireEventually.Equal(conditionTypeToStatus, actualConditionTypeToStatus, "unexpected statuses for conditions by type") + t.Logf("found FederationDomain %s/%s with conditions %#v, wanted condtions %#v", ns, name, actualConditionTypeToStatus, wantConditionTypeToStatus) + requireEventually.Equal(wantConditionTypeToStatus, actualConditionTypeToStatus, "unexpected statuses for conditions by type") }, 5*time.Minute, 200*time.Millisecond) } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 8b4728100..3fe6c70db 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -2078,11 +2078,21 @@ func testSupervisorLogin( // Create the downstream FederationDomain and expect it to go into the success status condition. downstream := testlib.CreateTestFederationDomain(ctx, t, - issuerURL.String(), - certSecret.Name, - configv1alpha1.FederationDomainPhaseReady, // TODO: expect another phase because this is a legacy FederationDomain and there is no IDP yet, so it is not safe to try to do logins until the IDP exists and the controller has a chance to run again to set the default IDP + configv1alpha1.FederationDomainSpec{ + Issuer: issuerURL.String(), + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + }, + // This is a legacy FederationDomain (does not explicitly list any IDPs) and there is no IDP yet, + // so it should not be ready yet. + configv1alpha1.FederationDomainPhaseError, ) + // Create upstream IDP and wait for it to become ready. + idpName := createIDP(t) + + // Now that both the FederationDomain and the IDP are created, the FederationDomain should be ready. + testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + // Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for // the `/jwks.json` endpoint to succeed, because there is no point in proceeding and eventually // calling the token endpoint from this test until the JWKS data has been loaded into @@ -2101,12 +2111,6 @@ func testSupervisorLogin( requireEventually.Equal(http.StatusOK, rsp.StatusCode) }, 30*time.Second, 200*time.Millisecond) - // Create upstream IDP and wait for it to become ready. - idpName := createIDP(t) - - // Now that both the FederationDomain and the IDP are created, the FederationDomain should be ready. - testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) - // Start a callback server on localhost. localCallbackServer := startLocalCallbackServer(t) diff --git a/test/integration/supervisor_secrets_test.go b/test/integration/supervisor_secrets_test.go index 51b1977ca..66321f590 100644 --- a/test/integration/supervisor_secrets_test.go +++ b/test/integration/supervisor_secrets_test.go @@ -6,6 +6,7 @@ package integration import ( "context" "encoding/json" + "fmt" "testing" "time" @@ -28,7 +29,12 @@ func TestSupervisorSecrets_Parallel(t *testing.T) { defer cancel() // Create our FederationDomain under test. - federationDomain := testlib.CreateTestFederationDomain(ctx, t, "", "", "") + federationDomain := testlib.CreateTestFederationDomain(ctx, t, + configv1alpha1.FederationDomainSpec{ + Issuer: fmt.Sprintf("http://test-issuer-%s.pinniped.dev", testlib.RandHex(t, 8)), + }, + configv1alpha1.FederationDomainPhaseError, // in phase error until there is an IDP created, but this test does not care + ) tests := []struct { name string diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index 6068ee8c4..9f6df59f2 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -83,9 +83,11 @@ func TestSupervisorWarnings_Browser(t *testing.T) { // Create the downstream FederationDomain and expect it to go into the success status condition. downstream := testlib.CreateTestFederationDomain(ctx, t, - issuerURL.String(), - certSecret.Name, - configv1alpha1.FederationDomainPhaseReady, + configv1alpha1.FederationDomainSpec{ + Issuer: issuerURL.String(), + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + }, + configv1alpha1.FederationDomainPhaseError, // in phase error until there is an IDP created ) // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. @@ -107,6 +109,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" @@ -251,6 +254,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { sAMAccountName := expectedUsername + "@" + env.SupervisorUpstreamActiveDirectory.Domain setupClusterForEndToEndActiveDirectoryTest(t, sAMAccountName, env) + testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" @@ -390,6 +394,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) + testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" diff --git a/test/testlib/client.go b/test/testlib/client.go index 4ab526a52..9fba764b2 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -267,16 +267,13 @@ func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alp } } -// CreateTestFederationDomain creates and returns a test FederationDomain in +// CreateTestFederationDomain creates and returns a test FederationDomain in the // $PINNIPED_TEST_SUPERVISOR_NAMESPACE, which will be automatically deleted at the end of the // current test's lifetime. -// If the provided issuer is not the empty string, then it will be used for the -// FederationDomain.Spec.Issuer field. Else, a random issuer will be generated. func CreateTestFederationDomain( ctx context.Context, t *testing.T, - issuer string, - certSecretName string, + spec configv1alpha1.FederationDomainSpec, expectStatus configv1alpha1.FederationDomainPhase, ) *configv1alpha1.FederationDomain { t.Helper() @@ -285,17 +282,10 @@ func CreateTestFederationDomain( createContext, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() - if issuer == "" { - issuer = fmt.Sprintf("http://test-issuer-%s.pinniped.dev", RandHex(t, 8)) - } - federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) federationDomain, err := federationDomainsClient.Create(createContext, &configv1alpha1.FederationDomain{ ObjectMeta: testObjectMeta(t, "oidc-provider"), - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: issuer, - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecretName}, - }, + Spec: spec, }, metav1.CreateOptions{}) require.NoError(t, err, "could not create test FederationDomain") t.Logf("created test FederationDomain %s/%s", federationDomain.Namespace, federationDomain.Name) @@ -313,11 +303,6 @@ func CreateTestFederationDomain( } }) - // If we're not expecting any particular status, just return the new FederationDomain immediately. - if expectStatus == "" { - return federationDomain - } - // Wait for the FederationDomain to enter the expected phase (or time out). WaitForTestFederationDomainStatus(ctx, t, federationDomain.Name, expectStatus) From e334ad6f7ef3406f0fbd8d0044e2ce4e56bd254a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 Jul 2023 09:42:20 -0700 Subject: [PATCH 33/81] Fix lint errors in federation_domain_watcher.go, and adjust unit test --- .../federation_domain_watcher.go | 68 +++---- .../federation_domain_watcher_test.go | 190 +++++++++++------- 2 files changed, 139 insertions(+), 119 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 60d196e43..939de443b 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -218,43 +218,31 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro idpNotFoundIndices := []int{} for index, idp := range federationDomain.Spec.IdentityProviders { - var idpResourceUID types.UID // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef // that does not resolve, put an error on the FederationDomain status. + var idpResourceUID types.UID + var foundIDP metav1.Object switch idp.ObjectRef.Kind { case "LDAPIdentityProvider": - ldapIDP, err := c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - if err == nil { - idpResourceUID = ldapIDP.UID - } else if errors.IsNotFound(err) { - idpNotFoundIndices = append(idpNotFoundIndices, index) - } else { - // TODO: handle unexpected errors - } + foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) case "ActiveDirectoryIdentityProvider": - adIDP, err := c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - if err == nil { - idpResourceUID = adIDP.UID - } else if errors.IsNotFound(err) { - idpNotFoundIndices = append(idpNotFoundIndices, index) - } else { - // TODO: handle unexpected errors - } + foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) case "OIDCIdentityProvider": - oidcIDP, err := c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - if err == nil { - idpResourceUID = oidcIDP.UID - } else if errors.IsNotFound(err) { - idpNotFoundIndices = append(idpNotFoundIndices, index) - } else { - // TODO: handle unexpected errors - } + foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) default: // TODO: handle an IDP type that we do not understand. } + switch { + case err == nil: + idpResourceUID = foundIDP.GetUID() + case errors.IsNotFound(err): + idpNotFoundIndices = append(idpNotFoundIndices, index) + default: + // TODO: handle unexpected errors + } // Prepare the transformations. pipeline := idtransform.NewTransformationPipeline() @@ -390,26 +378,24 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro if len(idpNotFoundIndices) != 0 { msgs := []string{} - for _, idpIndex := range idpNotFoundIndices { - idp := federationDomain.Spec.IdentityProviders[idpIndex] - displayName := idp.DisplayName - msgs = append(msgs, fmt.Sprintf("IDP with displayName %q at index %d", displayName, idpIndex)) + for _, idpNotFoundIndex := range idpNotFoundIndices { + msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, + federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) } + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", + strings.Join(msgs, ", ")), + }) + } else if len(federationDomain.Spec.IdentityProviders) != 0 { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProvidersObjectRefsNotFound, - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", strings.Join(msgs, ", ")), + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the resources specified by .spec.identityProviders[].objectRef were found", }) - } else { - if len(federationDomain.Spec.IdentityProviders) != 0 { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the resources specified by .spec.identityProviders[].objectRef were found", - }) - } } // Now that we have the list of IDPs for this FederationDomain, create the issuer. diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 459e2ee10..6b30818e2 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -133,18 +133,27 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenMetav1Now := metav1.NewTime(frozenNow) - identityProvider1 := &idpv1alpha1.OIDCIdentityProvider{ + oidcIdentityProvider := &idpv1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-name1", + Name: "some-oidc-idp", Namespace: namespace, - UID: "some-uid1", + UID: "some-oidc-uid", }, } - identityProvider2 := &idpv1alpha1.OIDCIdentityProvider{ + + ldapIdentityProvider := &idpv1alpha1.LDAPIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ldap-idp", + Namespace: namespace, + UID: "some-ldap-uid", + }, + } + + adIdentityProvider := &idpv1alpha1.ActiveDirectoryIdentityProvider{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-name2", + Name: "some-ad-idp", Namespace: namespace, - UID: "some-uid2", + UID: "some-ad-uid", }, } @@ -391,17 +400,17 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { require.Error(t, err) tests := []struct { - name string - inputObjects []runtime.Object - configPinnipedClient func(*pinnipedfake.Clientset) - wantErr string - wantStatusUpdates []*configv1alpha1.FederationDomain - wantFederationDomainIssuers []*federationdomainproviders.FederationDomainIssuer + name string + inputObjects []runtime.Object + configClient func(*pinnipedfake.Clientset) + wantErr string + wantStatusUpdates []*configv1alpha1.FederationDomain + wantFDIssuers []*federationdomainproviders.FederationDomainIssuer }{ { - name: "when there are no FederationDomains, no update actions happen and the list of FederationDomainIssuers is set to the empty list", - inputObjects: []runtime.Object{}, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + name: "when there are no FederationDomains, no update actions happen and the list of FederationDomainIssuers is set to the empty list", + inputObjects: []runtime.Object{}, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, }, { name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + @@ -410,20 +419,20 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ federationDomain1, federationDomain2, - identityProvider1, + oidcIdentityProvider, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -431,26 +440,26 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + "the out-of-date FederationDomain", inputObjects: []runtime.Object{ - identityProvider1, + oidcIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer}, Status: configv1alpha1.FederationDomainStatus{ Phase: configv1alpha1.FederationDomainPhaseReady, - Conditions: allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + Conditions: allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), }, }, federationDomain2, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider1.ObjectMeta), - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ // only one update, because the other FederationDomain already had the right status expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -459,9 +468,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ federationDomain1, federationDomain2, - identityProvider1, + oidcIdentityProvider, }, - configPinnipedClient: func(client *pinnipedfake.Clientset) { + configClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( "update", "federationdomains", @@ -475,18 +484,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ) }, wantErr: "could not update status: some update error", - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ // federationDomain1 is not included because it encountered an error - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -496,17 +505,17 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ invalidIssuerURLFederationDomain, federationDomain2, - identityProvider1, + oidcIdentityProvider, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -515,7 +524,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -525,9 +534,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { inputObjects: []runtime.Object{ invalidIssuerURLFederationDomain, federationDomain2, - identityProvider1, + oidcIdentityProvider, }, - configPinnipedClient: func(client *pinnipedfake.Clientset) { + configClient: func(client *pinnipedfake.Clientset) { client.PrependReactor( "update", "federationdomains", @@ -541,15 +550,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ) }, wantErr: "could not update status: some update error", - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ // only the valid FederationDomain - federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider1.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -558,7 +567,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -578,10 +587,10 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) }, - identityProvider1, + oidcIdentityProvider, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider1.ObjectMeta), + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( @@ -590,7 +599,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -603,7 +612,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -615,7 +624,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -653,10 +662,10 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, }, }, - identityProvider1, + oidcIdentityProvider, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ - federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider1.ObjectMeta), + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( @@ -665,7 +674,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -678,7 +687,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -691,7 +700,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider1.Name, frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -703,7 +712,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseReady, - allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider1.Name, frozenMetav1Now, 123), + allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", oidcIdentityProvider.Name, frozenMetav1Now, 123), ), }, }, @@ -713,7 +722,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { federationDomain1, federationDomain2, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, @@ -741,15 +750,16 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { name: "legacy config: no identity provider specified in federation domain and multiple identity providers found results in not specified status", inputObjects: []runtime.Object{ federationDomain1, - identityProvider1, - identityProvider2, + oidcIdentityProvider, + ldapIdentityProvider, + adIdentityProvider, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(2, frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -771,7 +781,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", - Name: "cant-find-me", + Name: "cant-find-me-name", }, }, { @@ -779,14 +789,22 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", - Name: "cant-find-me-either", + Name: "cant-find-me-either-name", + }, + }, + { + DisplayName: "cant-find-me-still", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "ActiveDirectoryIdentityProvider", + Name: "cant-find-me-still-name", }, }, }, }, }, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ @@ -796,7 +814,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { []configv1alpha1.Condition{ sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ - `IDP with displayName "cant-find-me" at index 0, IDP with displayName "cant-find-me-either" at index 1`, + `.spec.identityProviders[0] with displayName "cant-find-me", `+ + `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ + `.spec.identityProviders[2] with displayName "cant-find-me-still"`, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -809,8 +829,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { name: "the federation domain specifies identity providers that all exist", inputObjects: []runtime.Object{ - identityProvider1, - identityProvider2, + oidcIdentityProvider, + ldapIdentityProvider, + adIdentityProvider, &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: configv1alpha1.FederationDomainSpec{ @@ -821,32 +842,45 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), Kind: "OIDCIdentityProvider", - Name: identityProvider1.Name, + Name: oidcIdentityProvider.Name, }, }, { DisplayName: "can-find-me-too", ObjectRef: corev1.TypedLocalObjectReference{ APIGroup: pointer.String(apiGroupSupervisor), - Kind: "OIDCIdentityProvider", - Name: identityProvider2.Name, + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "can-find-me-three", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "ActiveDirectoryIdentityProvider", + Name: adIdentityProvider.Name, }, }, }, }, }, }, - wantFederationDomainIssuers: []*federationdomainproviders.FederationDomainIssuer{ + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ federationDomainIssuerWithIDPs(t, "https://issuer1.com", []*federationdomainproviders.FederationDomainIdentityProvider{ { DisplayName: "can-find-me", - UID: identityProvider1.UID, + UID: oidcIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, { DisplayName: "can-find-me-too", - UID: identityProvider2.UID, + UID: ldapIdentityProvider.UID, + Transforms: idtransform.NewTransformationPipeline(), + }, + { + DisplayName: "can-find-me-three", + UID: adIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, }), @@ -875,8 +909,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { require.NoError(t, pinnipedAPIClient.Tracker().Add(o)) require.NoError(t, pinnipedInformerClient.Tracker().Add(o)) } - if tt.configPinnipedClient != nil { - tt.configPinnipedClient(pinnipedAPIClient) + if tt.configClient != nil { + tt.configClient(pinnipedAPIClient) } pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0) @@ -905,9 +939,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { require.NoError(t, err) } - if tt.wantFederationDomainIssuers != nil { + if tt.wantFDIssuers != nil { require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) - require.ElementsMatch(t, tt.wantFederationDomainIssuers, federationDomainsSetter.FederationDomainsReceived) + require.ElementsMatch(t, tt.wantFDIssuers, federationDomainsSetter.FederationDomainsReceived) } else { require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) } From a38fb1629508c300a51603d71fe9ed9c0a88796f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 Jul 2023 10:57:11 -0700 Subject: [PATCH 34/81] Load FederationDomain endpoints before updating its status - Avoid a possible race condition where the status says "Ready" but the endpoints take another moment to become available, potentially casing a fast client to get a 404 after observing that the status is "Ready" and then immediately trying to use the endpoints. Co-authored-by: Benjamin A. Petersen --- .../federation_domain_watcher.go | 20 +++++++++++++++---- .../federation_domain_watcher_test.go | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 939de443b..3cf2becbd 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -134,6 +134,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro var errs []error federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) + fdToConditionsMap := map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition{} for _, federationDomain := range federationDomains { conditions := make([]*configv1alpha1.Condition, 0, 4) @@ -425,10 +426,10 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro }) } - if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) - continue - } + // Now that we have determined the conditions, save them for after the loop. + // For a valid FederationDomain, want to update the conditions after we have + // made the FederationDomain's endpoints available. + fdToConditionsMap[federationDomain] = conditions if !hadErrorCondition(conditions) { // Successfully validated the FederationDomain, so allow it to be loaded. @@ -436,8 +437,19 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } } + // Load the endpoints of every valid FederationDomain. Removes the endpoints of any + // previous FederationDomains which no longer exist or are no longer valid. c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) + // Now that the endpoints of every valid FederationDomain are available, update the + // statuses. This allows clients to wait for Ready without any race conditions in the + // endpoints being available. + for federationDomain, conditions := range fdToConditionsMap { + if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { + errs = append(errs, fmt.Errorf("could not update status: %w", err)) + } + } + return errorsutil.NewAggregate(errs) } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 6b30818e2..92e20c11e 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -485,7 +485,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, wantErr: "could not update status: some update error", wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ - // federationDomain1 is not included because it encountered an error + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta), }, wantStatusUpdates: []*configv1alpha1.FederationDomain{ From 76709892bc93e74617830fcd1462833bb8dc321f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 Jul 2023 13:25:08 -0700 Subject: [PATCH 35/81] Refactor: extract helper functions in federation_domain_watcher.go Co-authored-by: Benjamin A. Petersen --- .../federation_domain_watcher.go | 659 ++++++++++-------- 1 file changed, 364 insertions(+), 295 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 3cf2becbd..37cd203d1 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -10,6 +10,7 @@ import ( "strings" "time" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -68,6 +69,8 @@ type federationDomainWatcherController struct { oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer + + celTransformer *celtransformer.CELTransformer } // NewFederationDomainWatcherController creates a controllerlib.Controller that watches @@ -125,332 +128,398 @@ func NewFederationDomainWatcherController( } // Sync implements controllerlib.Syncer. -func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { //nolint:funlen,gocyclo +func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) error { federationDomains, err := c.federationDomainInformer.Lister().List(labels.Everything()) if err != nil { return err } + if c.celTransformer == nil { + c.celTransformer, _ = celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? + // TODO: handle err from NewCELTransformer() above + } + + // Process each FederationDomain to validate its spec and to turn it into a FederationDomainIssuer. + federationDomainIssuers, fdToConditionsMap, _ := c.processAllFederationDomains(federationDomains) + // TODO: handle err + + // Load the endpoints of every valid FederationDomain. Removes the endpoints of any + // previous FederationDomains which no longer exist or are no longer valid. + c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) + + // Now that the endpoints of every valid FederationDomain are available, update the + // statuses. This allows clients to wait for Ready without any race conditions in the + // endpoints being available. var errs []error + for federationDomain, conditions := range fdToConditionsMap { + if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { + errs = append(errs, fmt.Errorf("could not update status: %w", err)) + } + } + + return errorsutil.NewAggregate(errs) +} + +func (c *federationDomainWatcherController) processAllFederationDomains( + federationDomains []*configv1alpha1.FederationDomain, +) ([]*federationdomainproviders.FederationDomainIssuer, map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition, error) { federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) - crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) fdToConditionsMap := map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition{} + crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) for _, federationDomain := range federationDomains { - conditions := make([]*configv1alpha1.Condition, 0, 4) + conditions := make([]*configv1alpha1.Condition, 0) conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) - // TODO: Move all this identity provider stuff into helper functions. This is just a sketch of how the code would - // work in the sense that this is not doing error handling, is not validating everything that it should, and - // is not updating the status of the FederationDomain with anything related to these identity providers. - // This code may crash on invalid inputs since it is not handling any errors. However, when given valid inputs, - // this correctly implements the multiple IDPs features. - // Create the list of IDPs for this FederationDomain. - // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded - // into the provider cache, so they cannot actually be used to authenticate. - federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} - var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider - if len(federationDomain.Spec.IdentityProviders) == 0 { - // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. - oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) - ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) - activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) - // TODO handle err return value for each of the above three lines - - // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. - idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) - - switch { - case idpCRsCount == 1: - foundIDPName := "" - // If so, default that IDP's DisplayName to be the same as its resource Name. - defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} - switch { - case len(oidcIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID - foundIDPName = oidcIdentityProviders[0].Name - case len(ldapIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID - foundIDPName = ldapIdentityProviders[0].Name - case len(activeDirectoryIdentityProviders) == 1: - defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name - defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID - foundIDPName = activeDirectoryIdentityProviders[0].Name - } - // Backwards compatibility mode always uses an empty identity transformation pipeline since no - // transformations are defined on the FederationDomain. - defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonLegacyConfigurationSuccess, - Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ - "identity provider resource has been found: using %q as "+ - "identity provider: please explicitly list identity providers in .spec.identityProviders "+ - "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), - }) - case idpCRsCount > 1: - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific - Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ - "and %q identity provider resources have been found: "+ - "please update .spec.identityProviders to specify which identity providers "+ - "this federation domain should use", idpCRsCount), - }) - default: - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonLegacyConfigurationIdentityProviderNotFound, - Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + - "resources have been found: please create an identity provider resource", - }) - } + federationDomainIssuer, conditions, _ := c.makeFederationDomainIssuer(federationDomain, conditions) + // TODO: handle err + + // Now that we have determined the conditions, save them for after the loop. + // For a valid FederationDomain, want to update the conditions after we have + // made the FederationDomain's endpoints available. + fdToConditionsMap[federationDomain] = conditions + + if !hadErrorCondition(conditions) { + // Successfully validated the FederationDomain, so allow it to be loaded. + federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) } + } - // If there is an explicit list of IDPs on the FederationDomain, then process the list. - celTransformer, _ := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? - // TODO: handle err from NewCELTransformer() above + return federationDomainIssuers, fdToConditionsMap, nil +} - idpNotFoundIndices := []int{} - for index, idp := range federationDomain.Spec.IdentityProviders { - // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. - // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" - // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself - // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef - // that does not resolve, put an error on the FederationDomain status. - var idpResourceUID types.UID - var foundIDP metav1.Object - switch idp.ObjectRef.Kind { - case "LDAPIdentityProvider": - foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - case "ActiveDirectoryIdentityProvider": - foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - case "OIDCIdentityProvider": - foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(federationDomain.Namespace).Get(idp.ObjectRef.Name) - default: - // TODO: handle an IDP type that we do not understand. - } - switch { - case err == nil: - idpResourceUID = foundIDP.GetUID() - case errors.IsNotFound(err): - idpNotFoundIndices = append(idpNotFoundIndices, index) - default: - // TODO: handle unexpected errors - } +func (c *federationDomainWatcherController) makeFederationDomainIssuer( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + // Create the list of IDPs for this FederationDomain. + // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded + // into the provider cache, so they cannot actually be used to authenticate. + var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer + if len(federationDomain.Spec.IdentityProviders) == 0 { + federationDomainIssuer, conditions, _ = c.makeLegacyFederationDomainIssuer(federationDomain, conditions) + // TODO handle err + } else { + federationDomainIssuer, conditions, _ = c.makeFederationDomainIssuerWithExplicitIDPs(federationDomain, conditions) + // TODO handle err + } - // Prepare the transformations. - pipeline := idtransform.NewTransformationPipeline() - consts := &celtransformer.TransformationConstants{ - StringConstants: map[string]string{}, - StringListConstants: map[string][]string{}, - } - // Read all the declared constants. - for _, c := range idp.Transforms.Constants { - switch c.Type { - case "string": - consts.StringConstants[c.Name] = c.StringValue - case "stringList": - consts.StringListConstants[c.Name] = c.StringListValue - default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error - } - } - // Compile all the expressions and add them to the pipeline. - for idx, e := range idp.Transforms.Expressions { - var rawTransform celtransformer.CELTransformation - switch e.Type { - case "username/v1": - rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} - case "groups/v1": - rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} - case "policy/v1": - rawTransform = &celtransformer.AllowAuthenticationPolicy{ - Expression: e.Expression, - RejectedAuthenticationMessage: e.Message, - } - default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error - } - compiledTransform, err := celTransformer.CompileTransformation(rawTransform, consts) - if err != nil { - // TODO: handle compile err - plog.Error("error compiling identity transformation", err, - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "transformationIndex", idx, - "transformationType", e.Type, - "transformationExpression", e.Expression, - ) - } - pipeline.AppendTransformation(compiledTransform) - plog.Debug("successfully compiled identity transformation expression", - "type", e.Type, - "expr", e.Expression, - "policyMessage", e.Message, - ) - } - // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. - for idx, e := range idp.Transforms.Examples { - // TODO: use a real context param below - result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) - // TODO: handle err - resultWasAuthRejected := !result.AuthenticationAllowed - if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if !e.Expects.Rejected && resultWasAuthRejected { - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { - // TODO: when expected message is blank, then treat it like it expects the default message - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if result.AuthenticationAllowed { - // In the case where the user expected the auth to be allowed and it was allowed, then compare - // the expected username and group names to the actual username and group names. - // TODO: when both of these fail, put both errors onto the status (not just the first one) - if e.Expects.Username != result.Username { - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedUsername", e.Expects.Username, - "actualUsernameResult", result.Username, - ) - } - if !stringSlicesEqual(e.Expects.Groups, result.Groups) { - // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? - // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? - // TODO: handle this failed example - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", - "federationDomain", federationDomain.Name, - "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, - "expectedGroups", e.Expects.Groups, - "actualGroupsResult", result.Groups, - ) - } - } - } - // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. - federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ - DisplayName: idp.DisplayName, - UID: idpResourceUID, - Transforms: pipeline, - }) - plog.Debug("loaded FederationDomain identity provider", - "federationDomain", federationDomain.Name, - "identityProviderDisplayName", idp.DisplayName, - "identityProviderResourceUID", idpResourceUID, - ) - } + return federationDomainIssuer, conditions, nil +} - if len(idpNotFoundIndices) != 0 { - msgs := []string{} - for _, idpNotFoundIndex := range idpNotFoundIndices { - msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, - federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) - } - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProvidersObjectRefsNotFound, - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", - strings.Join(msgs, ", ")), - }) - } else if len(federationDomain.Spec.IdentityProviders) != 0 { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the resources specified by .spec.identityProviders[].objectRef were found", - }) +func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider + + // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. + oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) + ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) + activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) + // TODO handle err return value for each of the above three lines + + // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. + idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) + + switch { + case idpCRsCount == 1: + foundIDPName := "" + // If so, default that IDP's DisplayName to be the same as its resource Name. + defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} + switch { + case len(oidcIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID + foundIDPName = oidcIdentityProviders[0].Name + case len(ldapIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID + foundIDPName = ldapIdentityProviders[0].Name + case len(activeDirectoryIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID + foundIDPName = activeDirectoryIdentityProviders[0].Name } + // Backwards compatibility mode always uses an empty identity transformation pipeline since no + // transformations are defined on the FederationDomain. + defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonLegacyConfigurationSuccess, + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ + "identity provider resource has been found: using %q as "+ + "identity provider: please explicitly list identity providers in .spec.identityProviders "+ + "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), + }) + case idpCRsCount > 1: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific + Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ + "and %q identity provider resources have been found: "+ + "please update .spec.identityProviders to specify which identity providers "+ + "this federation domain should use", idpCRsCount), + }) + default: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonLegacyConfigurationIdentityProviderNotFound, + Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + + "resources have been found: please create an identity provider resource", + }) + } - // Now that we have the list of IDPs for this FederationDomain, create the issuer. - var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer - if defaultFederationDomainIdentityProvider != nil { - // This is the constructor for the backwards compatibility mode. - federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) - } else { - // This is the constructor for any other case, including when there is an empty list of IDPs. - federationDomainIssuer, err = federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) + // This is the constructor for the backwards compatibility mode. + federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) + conditions = appendIssuerURLValidCondition(err, conditions) + + return federationDomainIssuer, conditions, nil +} + +func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplicitIDPs( + federationDomain *configv1alpha1.FederationDomain, + conditions []*configv1alpha1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + var err error + federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} + idpNotFoundIndices := []int{} + + for index, idp := range federationDomain.Spec.IdentityProviders { + // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. + // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" + // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself + // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef + // that does not resolve, put an error on the FederationDomain status. + idpResourceUID, idpWasFound, _ := c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) + // TODO handle err + if !idpWasFound { + idpNotFoundIndices = append(idpNotFoundIndices, index) } - if err != nil { - // Note that the FederationDomainIssuer constructors only validate the Issuer URL, - // so these are always issuer URL validation errors. - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionFalse, - Reason: reasonInvalidIssuerURL, - Message: err.Error(), - }) - } else { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "spec.issuer is a valid URL", - }) + + pipeline, _ := c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name) + // TODO handle err + + // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. + federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ + DisplayName: idp.DisplayName, + UID: idpResourceUID, + Transforms: pipeline, + }) + plog.Debug("loaded FederationDomain identity provider", + "federationDomain", federationDomain.Name, + "identityProviderDisplayName", idp.DisplayName, + "identityProviderResourceUID", idpResourceUID, + ) + } + + if len(idpNotFoundIndices) != 0 { + msgs := []string{} + for _, idpNotFoundIndex := range idpNotFoundIndices { + msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, + federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) } + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", + strings.Join(msgs, ", ")), + }) + } else if len(federationDomain.Spec.IdentityProviders) != 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + }) + } - // Now that we have determined the conditions, save them for after the loop. - // For a valid FederationDomain, want to update the conditions after we have - // made the FederationDomain's endpoints available. - fdToConditionsMap[federationDomain] = conditions + // This is the constructor for any case other than the legacy case, including when there is an empty list of IDPs. + federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) + conditions = appendIssuerURLValidCondition(err, conditions) + return federationDomainIssuer, conditions, nil +} - if !hadErrorCondition(conditions) { - // Successfully validated the FederationDomain, so allow it to be loaded. - federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) +func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { + var idpResourceUID types.UID + var foundIDP metav1.Object + var err error + + switch objectRef.Kind { + case "LDAPIdentityProvider": + foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(namespace).Get(objectRef.Name) + case "ActiveDirectoryIdentityProvider": + foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(namespace).Get(objectRef.Name) + case "OIDCIdentityProvider": + foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(namespace).Get(objectRef.Name) + default: + // TODO: handle an IDP type that we do not understand. + } + + switch { + case err == nil: + idpResourceUID = foundIDP.GetUID() + case errors.IsNotFound(err): + return "", false, nil + default: + // TODO: handle unexpected errors + } + return idpResourceUID, true, nil +} + +func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( + idp configv1alpha1.FederationDomainIdentityProvider, + federationDomainName string, +) (*idtransform.TransformationPipeline, error) { + pipeline := idtransform.NewTransformationPipeline() + consts := &celtransformer.TransformationConstants{ + StringConstants: map[string]string{}, + StringListConstants: map[string][]string{}, + } + + // Read all the declared constants. + for _, c := range idp.Transforms.Constants { + switch c.Type { + case "string": + consts.StringConstants[c.Name] = c.StringValue + case "stringList": + consts.StringListConstants[c.Name] = c.StringListValue + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error } } - // Load the endpoints of every valid FederationDomain. Removes the endpoints of any - // previous FederationDomains which no longer exist or are no longer valid. - c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) + // Compile all the expressions and add them to the pipeline. + for idx, e := range idp.Transforms.Expressions { + var rawTransform celtransformer.CELTransformation + switch e.Type { + case "username/v1": + rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} + case "groups/v1": + rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} + case "policy/v1": + rawTransform = &celtransformer.AllowAuthenticationPolicy{ + Expression: e.Expression, + RejectedAuthenticationMessage: e.Message, + } + default: + // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + } + compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) + if err != nil { + // TODO: handle compile err + plog.Error("error compiling identity transformation", err, + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "transformationIndex", idx, + "transformationType", e.Type, + "transformationExpression", e.Expression, + ) + } + pipeline.AppendTransformation(compiledTransform) + plog.Debug("successfully compiled identity transformation expression", + "type", e.Type, + "expr", e.Expression, + "policyMessage", e.Message, + ) + } - // Now that the endpoints of every valid FederationDomain are available, update the - // statuses. This allows clients to wait for Ready without any race conditions in the - // endpoints being available. - for federationDomain, conditions := range fdToConditionsMap { - if err = c.updateStatus(ctx.Context, federationDomain, conditions); err != nil { - errs = append(errs, fmt.Errorf("could not update status: %w", err)) + // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. + for idx, e := range idp.Transforms.Examples { + // TODO: use a real context param below + result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) + // TODO: handle err + resultWasAuthRejected := !result.AuthenticationAllowed + if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if !e.Expects.Rejected && resultWasAuthRejected { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { + // TODO: when expected message is blank, then treat it like it expects the default message + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedRejected", e.Expects.Rejected, + "actualRejectedResult", resultWasAuthRejected, + "expectedMessage", e.Expects.Message, + "actualMessageResult", result.RejectedAuthenticationMessage, + ) + } else if result.AuthenticationAllowed { + // In the case where the user expected the auth to be allowed and it was allowed, then compare + // the expected username and group names to the actual username and group names. + // TODO: when both of these fail, put both errors onto the status (not just the first one) + if e.Expects.Username != result.Username { + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedUsername", e.Expects.Username, + "actualUsernameResult", result.Username, + ) + } + if !stringSlicesEqual(e.Expects.Groups, result.Groups) { + // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? + // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? + // TODO: handle this failed example + plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", + "federationDomain", federationDomainName, + "idpDisplayName", idp.DisplayName, + "exampleIndex", idx, + "expectedGroups", e.Expects.Groups, + "actualGroupsResult", result.Groups, + ) + } } } - return errorsutil.NewAggregate(errs) + return pipeline, nil +} + +func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if err != nil { + // Note that the FederationDomainIssuer constructors only validate the Issuer URL, + // so these are always issuer URL validation errors. + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonInvalidIssuerURL, + Message: err.Error(), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIssuerURLValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "spec.issuer is a valid URL", + }) + } + return conditions } func (c *federationDomainWatcherController) updateStatus( From a9f2f672c73a4283a93c188c8d60a1ed2d4aea68 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 Jul 2023 15:42:34 -0700 Subject: [PATCH 36/81] Handle some unexpected errors in federation_domain_watcher.go --- .../federation_domain_watcher.go | 100 +++++++++++------- .../federation_domain_watcher_test.go | 60 +++++++++++ 2 files changed, 122 insertions(+), 38 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 37cd203d1..c7f478b1b 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -135,13 +135,17 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } if c.celTransformer == nil { - c.celTransformer, _ = celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) // TODO: what is a good duration limit here? - // TODO: handle err from NewCELTransformer() above + c.celTransformer, err = celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) + if err != nil { + return err // shouldn't really happen + } } // Process each FederationDomain to validate its spec and to turn it into a FederationDomainIssuer. - federationDomainIssuers, fdToConditionsMap, _ := c.processAllFederationDomains(federationDomains) - // TODO: handle err + federationDomainIssuers, fdToConditionsMap, err := c.processAllFederationDomains(federationDomains) + if err != nil { + return err + } // Load the endpoints of every valid FederationDomain. Removes the endpoints of any // previous FederationDomains which no longer exist or are no longer valid. @@ -172,8 +176,10 @@ func (c *federationDomainWatcherController) processAllFederationDomains( conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) - federationDomainIssuer, conditions, _ := c.makeFederationDomainIssuer(federationDomain, conditions) - // TODO: handle err + federationDomainIssuer, conditions, err := c.makeFederationDomainIssuer(federationDomain, conditions) + if err != nil { + return nil, nil, err + } // Now that we have determined the conditions, save them for after the loop. // For a valid FederationDomain, want to update the conditions after we have @@ -193,16 +199,21 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuer( federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition, ) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + var err error // Create the list of IDPs for this FederationDomain. // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded // into the provider cache, so they cannot actually be used to authenticate. var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer if len(federationDomain.Spec.IdentityProviders) == 0 { - federationDomainIssuer, conditions, _ = c.makeLegacyFederationDomainIssuer(federationDomain, conditions) - // TODO handle err + federationDomainIssuer, conditions, err = c.makeLegacyFederationDomainIssuer(federationDomain, conditions) + if err != nil { + return nil, nil, err + } } else { - federationDomainIssuer, conditions, _ = c.makeFederationDomainIssuerWithExplicitIDPs(federationDomain, conditions) - // TODO handle err + federationDomainIssuer, conditions, err = c.makeFederationDomainIssuerWithExplicitIDPs(federationDomain, conditions) + if err != nil { + return nil, nil, err + } } return federationDomainIssuer, conditions, nil @@ -215,10 +226,18 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. - oidcIdentityProviders, _ := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) - ldapIdentityProviders, _ := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) - activeDirectoryIdentityProviders, _ := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) - // TODO handle err return value for each of the above three lines + oidcIdentityProviders, err := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, nil, err + } + ldapIdentityProviders, err := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, nil, err + } + activeDirectoryIdentityProviders, err := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, nil, err + } // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) @@ -285,7 +304,6 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition, ) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { - var err error federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} idpNotFoundIndices := []int{} @@ -295,14 +313,18 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef // that does not resolve, put an error on the FederationDomain status. - idpResourceUID, idpWasFound, _ := c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) - // TODO handle err + idpResourceUID, idpWasFound, err := c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) + if err != nil { + return nil, nil, err + } if !idpWasFound { idpNotFoundIndices = append(idpNotFoundIndices, index) } - pipeline, _ := c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name) - // TODO handle err + pipeline, err := c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name) + if err != nil { + return nil, nil, err + } // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ @@ -358,7 +380,7 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor case "OIDCIdentityProvider": foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(namespace).Get(objectRef.Name) default: - // TODO: handle an IDP type that we do not understand. + // TODO: handle an IDP type that we do not understand by writing a status condition } switch { @@ -367,7 +389,7 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor case errors.IsNotFound(err): return "", false, nil default: - // TODO: handle unexpected errors + return "", false, err // unexpected error } return idpResourceUID, true, nil } @@ -383,32 +405,34 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit } // Read all the declared constants. - for _, c := range idp.Transforms.Constants { - switch c.Type { + for _, constant := range idp.Transforms.Constants { + switch constant.Type { case "string": - consts.StringConstants[c.Name] = c.StringValue + consts.StringConstants[constant.Name] = constant.StringValue case "stringList": - consts.StringListConstants[c.Name] = c.StringListValue + consts.StringListConstants[constant.Name] = constant.StringListValue default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + // This shouldn't really happen since the CRD validates it, but handle it as an error. + return nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) } } // Compile all the expressions and add them to the pipeline. - for idx, e := range idp.Transforms.Expressions { + for idx, expr := range idp.Transforms.Expressions { var rawTransform celtransformer.CELTransformation - switch e.Type { + switch expr.Type { case "username/v1": - rawTransform = &celtransformer.UsernameTransformation{Expression: e.Expression} + rawTransform = &celtransformer.UsernameTransformation{Expression: expr.Expression} case "groups/v1": - rawTransform = &celtransformer.GroupsTransformation{Expression: e.Expression} + rawTransform = &celtransformer.GroupsTransformation{Expression: expr.Expression} case "policy/v1": rawTransform = &celtransformer.AllowAuthenticationPolicy{ - Expression: e.Expression, - RejectedAuthenticationMessage: e.Message, + Expression: expr.Expression, + RejectedAuthenticationMessage: expr.Message, } default: - // TODO: this shouldn't really happen since the CRD validates it, but handle it as an error + // This shouldn't really happen since the CRD validates it, but handle it as an error. + return nil, fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) } compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) if err != nil { @@ -417,15 +441,15 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, "transformationIndex", idx, - "transformationType", e.Type, - "transformationExpression", e.Expression, + "transformationType", expr.Type, + "transformationExpression", expr.Expression, ) } pipeline.AppendTransformation(compiledTransform) plog.Debug("successfully compiled identity transformation expression", - "type", e.Type, - "expr", e.Expression, - "policyMessage", e.Message, + "type", expr.Type, + "expr", expr.Expression, + "policyMessage", expr.Message, ) } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 92e20c11e..6d7f75b8a 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -895,6 +895,66 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "can-find-me", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + { + Type: "this is illegal", + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: `one of spec.identityProvider[].transforms.constants[].type is invalid: "this is illegal"`, + }, + { + name: "the federation domain specifies illegal expression type, which shouldn't really happen since the CRD validates it", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "can-find-me", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "this is illegal", + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: `one of spec.identityProvider[].transforms.expressions[].type is invalid: "this is illegal"`, + }, } for _, tt := range tests { From 31d67a1af33549c6a9856b17f88ccd33eb6af255 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 Jul 2023 09:43:33 -0700 Subject: [PATCH 37/81] Validate display names are unique in federation_domain_watcher.go --- .../federation_domain_watcher.go | 41 ++++++- .../federation_domain_watcher_test.go | 107 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index c7f478b1b..44d3f9fc9 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net/url" + "sort" "strings" "time" @@ -17,6 +18,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" errorsutil "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/clock" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -38,6 +40,7 @@ const ( typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" typeIssuerIsUnique = "IssuerIsUnique" typeIdentityProvidersFound = "IdentityProvidersFound" + typeDisplayNamesUnique = "DisplayNamesUnique" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -49,6 +52,7 @@ const ( reasonLegacyConfigurationIdentityProviderNotFound = "LegacyConfigurationIdentityProviderNotFound" reasonIdentityProvidersObjectRefsNotFound = "IdentityProvidersObjectRefsNotFound" reasonIdentityProviderNotSpecified = "IdentityProviderNotSpecified" + reasonDuplicateDisplayNames = "DuplicateDisplayNames" celTransformerMaxExpressionRuntime = 5 * time.Second ) @@ -293,6 +297,8 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( }) } + conditions = c.addDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) + // This is the constructor for the backwards compatibility mode. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) conditions = appendIssuerURLValidCondition(err, conditions) @@ -306,10 +312,17 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic ) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} idpNotFoundIndices := []int{} + displayNames := sets.Set[string]{} + duplicateDisplayNames := sets.Set[string]{} for index, idp := range federationDomain.Spec.IdentityProviders { - // TODO: Validate that all displayNames are unique within this FederationDomain's spec's list of identity providers. - // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" + if displayNames.Has(idp.DisplayName) { + duplicateDisplayNames.Insert(idp.DisplayName) + } + displayNames.Insert(idp.DisplayName) + + // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" where .pinniped.dev is the configurable suffix + // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef // that does not resolve, put an error on the FederationDomain status. @@ -361,12 +374,36 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic }) } + conditions = c.addDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) + // This is the constructor for any case other than the legacy case, including when there is an empty list of IDPs. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) conditions = appendIssuerURLValidCondition(err, conditions) return federationDomainIssuer, conditions, nil } +func (c *federationDomainWatcherController) addDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if duplicateDisplayNames.Len() > 0 { + duplicates := duplicateDisplayNames.UnsortedList() + sort.Strings(duplicates) + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeDisplayNamesUnique, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDuplicateDisplayNames, + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", + strings.Join(duplicates, ", ")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeDisplayNamesUnique, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the names specified by .spec.identityProviders[].displayName are unique", + }) + } + return conditions +} + func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { var idpResourceUID types.UID var foundIDP metav1.Object diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 6d7f75b8a..c460f58d1 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -373,9 +373,32 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "DisplayNamesUnique", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the names specified by .spec.identityProviders[].displayName are unique", + } + } + + sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "DisplayNamesUnique", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DuplicateDisplayNames", + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", duplicateNames), + } + } + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ // expect them to be sorted alphabetically by type + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), happyIssuerIsUniqueCondition(time, observedGeneration), happyIssuerURLValidCondition(time, observedGeneration), @@ -387,6 +410,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ // expect them to be sorted alphabetically by type + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -515,6 +539,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), @@ -558,6 +583,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), @@ -599,6 +625,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -612,6 +639,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -674,6 +702,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -687,6 +716,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -700,6 +730,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), @@ -727,6 +758,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -737,6 +769,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -759,6 +792,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), @@ -812,6 +846,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ `.spec.identityProviders[0] with displayName "cant-find-me", `+ @@ -895,6 +930,78 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "the federation domain has duplicate display names for IDPs", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + ldapIdentityProvider, + adIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "duplicate1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + }, + { + DisplayName: "duplicate1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "ActiveDirectoryIdentityProvider", + Name: adIdentityProvider.Name, + }, + }, + { + DisplayName: "duplicate2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "duplicate2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "ActiveDirectoryIdentityProvider", + Name: adIdentityProvider.Name, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadDisplayNamesUniqueCondition("duplicate1, duplicate2", frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + }, + }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ From 32063db46e75fd865ad2083b16e18dcede0e2168 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 Jul 2023 10:34:15 -0700 Subject: [PATCH 38/81] Validate apiGroup names are valid in federation_domain_watcher.go --- .../federation_domain_watcher.go | 91 ++++++++++----- .../federation_domain_watcher_test.go | 106 +++++++++++++++++- internal/supervisor/server/server.go | 1 + 3 files changed, 170 insertions(+), 28 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 44d3f9fc9..91780b92a 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -41,6 +41,7 @@ const ( typeIssuerIsUnique = "IssuerIsUnique" typeIdentityProvidersFound = "IdentityProvidersFound" typeDisplayNamesUnique = "DisplayNamesUnique" + typeAPIGroupSuffixValid = "APIGroupSuffixValid" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -53,6 +54,7 @@ const ( reasonIdentityProvidersObjectRefsNotFound = "IdentityProvidersObjectRefsNotFound" reasonIdentityProviderNotSpecified = "IdentityProviderNotSpecified" reasonDuplicateDisplayNames = "DuplicateDisplayNames" + reasonAPIGroupNameUnrecognized = "APIGroupNameUnrecognized" celTransformerMaxExpressionRuntime = 5 * time.Second ) @@ -66,6 +68,7 @@ type FederationDomainsSetter interface { type federationDomainWatcherController struct { federationDomainsSetter FederationDomainsSetter + apiGroup string clock clock.Clock client pinnipedclientset.Interface @@ -81,6 +84,7 @@ type federationDomainWatcherController struct { // FederationDomain objects and notifies a callback object of the collection of provider configs. func NewFederationDomainWatcherController( federationDomainsSetter FederationDomainsSetter, + apiGroupSuffix string, clock clock.Clock, client pinnipedclientset.Interface, federationDomainInformer configinformers.FederationDomainInformer, @@ -94,6 +98,7 @@ func NewFederationDomainWatcherController( Name: "FederationDomainWatcherController", Syncer: &federationDomainWatcherController{ federationDomainsSetter: federationDomainsSetter, + apiGroup: fmt.Sprintf("idp.supervisor.%s", apiGroupSuffix), clock: clock, client: client, federationDomainInformer: federationDomainInformer, @@ -297,12 +302,13 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( }) } - conditions = c.addDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) - // This is the constructor for the backwards compatibility mode. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) conditions = appendIssuerURLValidCondition(err, conditions) + conditions = appendDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) + conditions = appendAPIGroupSuffixCondition(c.apiGroup, sets.Set[string]{}, conditions) + return federationDomainIssuer, conditions, nil } @@ -314,6 +320,7 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic idpNotFoundIndices := []int{} displayNames := sets.Set[string]{} duplicateDisplayNames := sets.Set[string]{} + badAPIGroupNames := sets.Set[string]{} for index, idp := range federationDomain.Spec.IdentityProviders { if displayNames.Has(idp.DisplayName) { @@ -321,7 +328,13 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic } displayNames.Insert(idp.DisplayName) - // TODO: Validate that idp.ObjectRef.APIGroup is the expected APIGroup for IDP CRs "idp.supervisor.pinniped.dev" where .pinniped.dev is the configurable suffix + apiGroup := "nil" + if idp.ObjectRef.APIGroup != nil { + apiGroup = *idp.ObjectRef.APIGroup + } + if apiGroup != c.apiGroup { + badAPIGroupNames.Insert(apiGroup) + } // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef @@ -374,34 +387,14 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic }) } - conditions = c.addDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) - // This is the constructor for any case other than the legacy case, including when there is an empty list of IDPs. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) conditions = appendIssuerURLValidCondition(err, conditions) - return federationDomainIssuer, conditions, nil -} -func (c *federationDomainWatcherController) addDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if duplicateDisplayNames.Len() > 0 { - duplicates := duplicateDisplayNames.UnsortedList() - sort.Strings(duplicates) - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeDisplayNamesUnique, - Status: configv1alpha1.ConditionFalse, - Reason: reasonDuplicateDisplayNames, - Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", - strings.Join(duplicates, ", ")), - }) - } else { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeDisplayNamesUnique, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the names specified by .spec.identityProviders[].displayName are unique", - }) - } - return conditions + conditions = appendDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) + conditions = appendAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) + + return federationDomainIssuer, conditions, nil } func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { @@ -562,6 +555,50 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit return pipeline, nil } +func appendAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if badSuffixNames.Len() > 0 { + badNames := badSuffixNames.UnsortedList() + sort.Strings(badNames) + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeAPIGroupSuffixValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonAPIGroupNameUnrecognized, + Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be %q): %s", + expectedSuffixName, strings.Join(badNames, ", ")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeAPIGroupSuffixValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", + }) + } + return conditions +} + +func appendDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if duplicateDisplayNames.Len() > 0 { + duplicates := duplicateDisplayNames.UnsortedList() + sort.Strings(duplicates) + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeDisplayNamesUnique, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDuplicateDisplayNames, + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", + strings.Join(duplicates, ", ")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeDisplayNamesUnique, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the names specified by .spec.identityProviders[].displayName are unique", + }) + } + return conditions +} + func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if err != nil { // Note that the FederationDomainIssuer constructors only validate the Issuer URL, diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index c460f58d1..10884cc78 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -89,6 +89,7 @@ func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { NewFederationDomainWatcherController( nil, + "", nil, nil, federationDomainInformer, @@ -128,7 +129,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { t.Parallel() const namespace = "some-namespace" - const apiGroupSupervisor = "idp.supervisor.pinniped.dev" + const apiGroupSuffix = "custom.suffix.pinniped.dev" + const apiGroupSupervisor = "idp.supervisor." + apiGroupSuffix frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenMetav1Now := metav1.NewTime(frozenNow) @@ -395,9 +397,32 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "APIGroupSuffixValid", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", + } + } + + sadAPIGroupSuffixCondition := func(badNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "APIGroupSuffixValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "APIGroupNameUnrecognized", + Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badNames), + } + } + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ // expect them to be sorted alphabetically by type + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), happyIssuerIsUniqueCondition(time, observedGeneration), @@ -410,6 +435,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return []configv1alpha1.Condition{ // expect them to be sorted alphabetically by type + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -539,6 +565,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -583,6 +610,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -625,6 +653,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -639,6 +668,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -702,6 +732,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -716,6 +747,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -730,6 +762,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -758,6 +791,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -769,6 +803,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -792,6 +827,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -846,6 +882,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ @@ -993,6 +1030,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, []configv1alpha1.Condition{ + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), sadDisplayNamesUniqueCondition("duplicate1, duplicate2", frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), @@ -1002,6 +1040,71 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }), }, }, + { + name: "the federation domain has unrecognized api group names in objectRefs", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + ldapIdentityProvider, + adIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("wrong.example.com"), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + }, + { + DisplayName: "name2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("also-wrong.example.com"), + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "name3", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: nil, // also wrong + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "name4", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), // correct + Kind: "ActiveDirectoryIdentityProvider", + Name: adIdentityProvider.Name, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + []configv1alpha1.Condition{ + sadAPIGroupSuffixCondition("also-wrong.example.com, nil, wrong.example.com", frozenMetav1Now, 123), + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + }, + }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ @@ -1083,6 +1186,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { controller := NewFederationDomainWatcherController( federationDomainsSetter, + apiGroupSuffix, clocktesting.NewFakeClock(frozenNow), pinnipedAPIClient, pinnipedInformers.Config().V1alpha1().FederationDomains(), diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 153869b01..d6605b2b3 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -167,6 +167,7 @@ func prepareControllers( WithController( supervisorconfig.NewFederationDomainWatcherController( issuerManager, + *cfg.APIGroupSuffix, clock.RealClock{}, pinnipedClient, federationDomainInformer, From 8e169f970213d3b9ed93f87a633e44e9d571c893 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 Jul 2023 13:15:52 -0700 Subject: [PATCH 39/81] Validate IDP objectRef kind names in federation_domain_watcher.go Co-authored-by: Benjamin A. Petersen --- .../federation_domain_watcher.go | 139 ++++++++---- .../federation_domain_watcher_test.go | 206 ++++++++++++++---- test/integration/supervisor_discovery_test.go | 52 +++-- 3 files changed, 288 insertions(+), 109 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 91780b92a..81e438005 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -35,13 +35,14 @@ import ( ) const ( - typeReady = "Ready" - typeIssuerURLValid = "IssuerURLValid" - typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" - typeIssuerIsUnique = "IssuerIsUnique" - typeIdentityProvidersFound = "IdentityProvidersFound" - typeDisplayNamesUnique = "DisplayNamesUnique" - typeAPIGroupSuffixValid = "APIGroupSuffixValid" + typeReady = "Ready" + typeIssuerURLValid = "IssuerURLValid" + typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" + typeIssuerIsUnique = "IssuerIsUnique" + typeIdentityProvidersFound = "IdentityProvidersFound" + typeIdentityProvidersDisplayNamesUnique = "IdentityProvidersDisplayNamesUnique" + typeIdentityProvidersAPIGroupSuffixValid = "IdentityProvidersObjectRefAPIGroupSuffixValid" + typeIdentityProvidersObjectRefKindValid = "IdentityProvidersObjectRefKindValid" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -54,7 +55,12 @@ const ( reasonIdentityProvidersObjectRefsNotFound = "IdentityProvidersObjectRefsNotFound" reasonIdentityProviderNotSpecified = "IdentityProviderNotSpecified" reasonDuplicateDisplayNames = "DuplicateDisplayNames" - reasonAPIGroupNameUnrecognized = "APIGroupNameUnrecognized" + reasonAPIGroupNameUnrecognized = "APIGroupUnrecognized" + reasonKindUnrecognized = "KindUnrecognized" + + kindLDAPIdentityProvider = "LDAPIdentityProvider" + kindOIDCIdentityProvider = "OIDCIdentityProvider" + kindActiveDirectoryIdentityProvider = "ActiveDirectoryIdentityProvider" celTransformerMaxExpressionRuntime = 5 * time.Second ) @@ -78,6 +84,7 @@ type federationDomainWatcherController struct { activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer celTransformer *celtransformer.CELTransformer + allowedKinds sets.Set[string] } // NewFederationDomainWatcherController creates a controllerlib.Controller that watches @@ -93,6 +100,7 @@ func NewFederationDomainWatcherController( activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { + allowedKinds := sets.New(kindActiveDirectoryIdentityProvider, kindLDAPIdentityProvider, kindOIDCIdentityProvider) return controllerlib.New( controllerlib.Config{ Name: "FederationDomainWatcherController", @@ -105,6 +113,7 @@ func NewFederationDomainWatcherController( oidcIdentityProviderInformer: oidcIdentityProviderInformer, ldapIdentityProviderInformer: ldapIdentityProviderInformer, activeDirectoryIdentityProviderInformer: activeDirectoryIdentityProviderInformer, + allowedKinds: allowedKinds, }, }, withInformer( @@ -306,8 +315,9 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) conditions = appendIssuerURLValidCondition(err, conditions) - conditions = appendDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) - conditions = appendAPIGroupSuffixCondition(c.apiGroup, sets.Set[string]{}, conditions) + conditions = appendIdentityProviderDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) + conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, []string{}, conditions) + conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) return federationDomainIssuer, conditions, nil } @@ -320,30 +330,47 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic idpNotFoundIndices := []int{} displayNames := sets.Set[string]{} duplicateDisplayNames := sets.Set[string]{} - badAPIGroupNames := sets.Set[string]{} + badAPIGroupNames := []string{} + badKinds := []string{} for index, idp := range federationDomain.Spec.IdentityProviders { + // The CRD requires the displayName field, and validates that it has at least one character, + // so here we only need to validate that they are unique. if displayNames.Has(idp.DisplayName) { duplicateDisplayNames.Insert(idp.DisplayName) } displayNames.Insert(idp.DisplayName) - apiGroup := "nil" + // The objectRef is a required field in the CRD, so it will always exist in practice. + // objectRef.name and objectRef.kind are required, but may be empty strings. + // objectRef.apiGroup is not required, however, so it may be nil or empty string. + canTryToFindIDP := true + apiGroup := "" if idp.ObjectRef.APIGroup != nil { apiGroup = *idp.ObjectRef.APIGroup } if apiGroup != c.apiGroup { - badAPIGroupNames.Insert(apiGroup) + badAPIGroupNames = append(badAPIGroupNames, apiGroup) + canTryToFindIDP = false + } + if !c.allowedKinds.Has(idp.ObjectRef.Kind) { + badKinds = append(badKinds, idp.ObjectRef.Kind) + canTryToFindIDP = false } - // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself - // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef - // that does not resolve, put an error on the FederationDomain status. - idpResourceUID, idpWasFound, err := c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) - if err != nil { - return nil, nil, err + var idpResourceUID types.UID + idpWasFound := false + if canTryToFindIDP { + var err error + // Validate that each objectRef resolves to an existing IDP. It does not matter if the IDP itself + // is phase=Ready, because it will not be loaded into the cache if not ready. For each objectRef + // that does not resolve, put an error on the FederationDomain status. + idpResourceUID, idpWasFound, err = c.findIDPsUIDByObjectRef(idp.ObjectRef, federationDomain.Namespace) + if err != nil { + return nil, nil, err + } } - if !idpWasFound { + if !canTryToFindIDP || !idpWasFound { idpNotFoundIndices = append(idpNotFoundIndices, index) } @@ -391,26 +418,27 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) conditions = appendIssuerURLValidCondition(err, conditions) - conditions = appendDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) - conditions = appendAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) + conditions = appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) + conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) + conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), badKinds, conditions) return federationDomainIssuer, conditions, nil } - func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { var idpResourceUID types.UID var foundIDP metav1.Object var err error switch objectRef.Kind { - case "LDAPIdentityProvider": + case kindLDAPIdentityProvider: foundIDP, err = c.ldapIdentityProviderInformer.Lister().LDAPIdentityProviders(namespace).Get(objectRef.Name) - case "ActiveDirectoryIdentityProvider": + case kindActiveDirectoryIdentityProvider: foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(namespace).Get(objectRef.Name) - case "OIDCIdentityProvider": + case kindOIDCIdentityProvider: foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(namespace).Get(objectRef.Name) default: - // TODO: handle an IDP type that we do not understand by writing a status condition + // This shouldn't happen because this helper function is not called when the kind is invalid. + return "", false, fmt.Errorf("unexpected kind: %s", objectRef.Kind) } switch { @@ -419,7 +447,7 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor case errors.IsNotFound(err): return "", false, nil default: - return "", false, err // unexpected error + return "", false, err // unexpected error from the informer } return idpResourceUID, true, nil } @@ -555,20 +583,42 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit return pipeline, nil } -func appendAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if badSuffixNames.Len() > 0 { - badNames := badSuffixNames.UnsortedList() - sort.Strings(badNames) +func (c *federationDomainWatcherController) sortedAllowedKinds() []string { + return sortAndQuote(c.allowedKinds.UnsortedList()) +} + +func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(badSuffixNames) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeAPIGroupSuffixValid, + Type: typeIdentityProvidersObjectRefKindValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonKindUnrecognized, + Message: fmt.Sprintf("the kinds specified by .spec.identityProviders[].objectRef.kind are not recognized (should be one of %s): %s", + strings.Join(expectedKinds, ", "), strings.Join(sortAndQuote(badSuffixNames), ", ")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersObjectRefKindValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", + }) + } + return conditions +} + +func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(badSuffixNames) > 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersAPIGroupSuffixValid, Status: configv1alpha1.ConditionFalse, Reason: reasonAPIGroupNameUnrecognized, Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be %q): %s", - expectedSuffixName, strings.Join(badNames, ", ")), + expectedSuffixName, strings.Join(sortAndQuote(badSuffixNames), ", ")), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeAPIGroupSuffixValid, + Type: typeIdentityProvidersAPIGroupSuffixValid, Status: configv1alpha1.ConditionTrue, Reason: reasonSuccess, Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", @@ -577,20 +627,18 @@ func appendAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames set return conditions } -func appendDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if duplicateDisplayNames.Len() > 0 { - duplicates := duplicateDisplayNames.UnsortedList() - sort.Strings(duplicates) conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeDisplayNamesUnique, + Type: typeIdentityProvidersDisplayNamesUnique, Status: configv1alpha1.ConditionFalse, Reason: reasonDuplicateDisplayNames, Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", - strings.Join(duplicates, ", ")), + strings.Join(sortAndQuote(duplicateDisplayNames.UnsortedList()), ", ")), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeDisplayNamesUnique, + Type: typeIdentityProvidersDisplayNamesUnique, Status: configv1alpha1.ConditionTrue, Reason: reasonSuccess, Message: "the names specified by .spec.identityProviders[].displayName are unique", @@ -620,6 +668,15 @@ func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condi return conditions } +func sortAndQuote(strs []string) []string { + quoted := make([]string, 0, len(strs)) + for _, s := range strs { + quoted = append(quoted, fmt.Sprintf("%q", s)) + } + sort.Strings(quoted) + return quoted +} + func (c *federationDomainWatcherController) updateStatus( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 10884cc78..8cad14e11 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -364,20 +364,20 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(msg string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(idpsNotFound string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "IdentityProvidersObjectRefsNotFound", - Message: msg, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", idpsNotFound), } } happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ - Type: "DisplayNamesUnique", + Type: "IdentityProvidersDisplayNamesUnique", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -388,7 +388,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ - Type: "DisplayNamesUnique", + Type: "IdentityProvidersDisplayNamesUnique", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -399,7 +399,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ - Type: "APIGroupSuffixValid", + Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "True", ObservedGeneration: observedGeneration, LastTransitionTime: time, @@ -408,20 +408,53 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadAPIGroupSuffixCondition := func(badNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + sadAPIGroupSuffixCondition := func(badApiGroups string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ - Type: "APIGroupSuffixValid", + Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, - Reason: "APIGroupNameUnrecognized", - Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badNames), + Reason: "APIGroupUnrecognized", + Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup "+ + "are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badApiGroups), } } + happyKindCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersObjectRefKindValid", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", + } + } + + sadKindCondition := func(badKinds string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "IdentityProvidersObjectRefKindValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "KindUnrecognized", + Message: fmt.Sprintf(`the kinds specified by .spec.identityProviders[].objectRef.kind are `+ + `not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds), + } + } + + sortConditionsByType := func(c []configv1alpha1.Condition) []configv1alpha1.Condition { + cp := make([]configv1alpha1.Condition, len(c)) + copy(cp, c) + sort.SliceStable(cp, func(i, j int) bool { + return cp[i].Type < cp[j].Type + }) + return cp + } + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { - return []configv1alpha1.Condition{ - // expect them to be sorted alphabetically by type + return sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), @@ -429,12 +462,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(time, observedGeneration), happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), happyReadyCondition(issuer, time, observedGeneration), - } + }) } allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { - return []configv1alpha1.Condition{ - // expect them to be sorted alphabetically by type + return sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), @@ -442,7 +475,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyReadyCondition(issuer, frozenMetav1Now, 123), - } + }) } invalidIssuerURL := ":/host//path" @@ -564,7 +597,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -572,7 +606,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, @@ -609,7 +643,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -617,7 +652,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, @@ -652,7 +687,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -660,14 +696,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -675,7 +712,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ @@ -731,7 +768,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -739,14 +777,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -754,14 +793,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), @@ -769,7 +809,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ @@ -790,7 +830,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), @@ -798,11 +839,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), @@ -810,7 +852,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), }, }, @@ -826,7 +868,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), @@ -834,7 +877,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), }, }, @@ -881,12 +924,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: `+ - `.spec.identityProviders[0] with displayName "cant-find-me", `+ + `.spec.identityProviders[0] with displayName "cant-find-me", `+ `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ `.spec.identityProviders[2] with displayName "cant-find-me-still"`, frozenMetav1Now, 123), @@ -894,7 +937,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }, + }), ), }, }, @@ -1029,15 +1072,16 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - sadDisplayNamesUniqueCondition("duplicate1, duplicate2", frozenMetav1Now, 123), + sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }), + })), }, }, { @@ -1062,7 +1106,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("also-wrong.example.com"), + APIGroup: pointer.String(""), // empty string is wrong Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1070,7 +1114,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name3", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: nil, // also wrong + APIGroup: nil, // nil is wrong, and gets treated like an empty string in the error condition Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1094,15 +1138,81 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - []configv1alpha1.Condition{ - sadAPIGroupSuffixCondition("also-wrong.example.com, nil, wrong.example.com", frozenMetav1Now, 123), + sortConditionsByType([]configv1alpha1.Condition{ + happyKindCondition(frozenMetav1Now, 123), + sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[0] with displayName "name1", `+ + `.spec.identityProviders[1] with displayName "name2", `+ + `.spec.identityProviders[2] with displayName "name3"`, + frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), - }), + })), + }, + }, + { + name: "the federation domain has unrecognized kind names in objectRefs", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + ldapIdentityProvider, + adIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", // correct + Name: oidcIdentityProvider.Name, + }, + }, + { + DisplayName: "name2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "wrong", + Name: ldapIdentityProvider.Name, + }, + }, + { + DisplayName: "name3", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "", // empty is also wrong + Name: ldapIdentityProvider.Name, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + sortConditionsByType([]configv1alpha1.Condition{ + sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[1] with displayName "name2", `+ + `.spec.identityProviders[2] with displayName "name3"`, + frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + })), }, }, { diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index c971584a3..9e2b7807e 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -133,18 +133,24 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuer6}, v1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionFalse, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionFalse, + "IdentityProvidersFound": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, + "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, }) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionFalse, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionFalse, + "IdentityProvidersFound": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, + "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) @@ -164,11 +170,14 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When we create a provider with an invalid issuer, the status is set to invalid. badConfig := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: badIssuer}, v1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, badConfig.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionTrue, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionFalse, + "Ready": v1alpha1.ConditionFalse, + "IssuerIsUnique": v1alpha1.ConditionTrue, + "IdentityProvidersFound": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionFalse, + "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, + "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) @@ -677,11 +686,14 @@ func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name st func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string) { requireStatus(t, client, ns, name, v1alpha1.FederationDomainPhaseReady, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionTrue, - "IssuerIsUnique": v1alpha1.ConditionTrue, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, + "Ready": v1alpha1.ConditionTrue, + "IssuerIsUnique": v1alpha1.ConditionTrue, + "IdentityProvidersFound": v1alpha1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, + "IssuerURLValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, + "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, + "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, }) } From b05e8a5e246e18857aee6a5e9721788b36700be2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 Jul 2023 14:49:44 -0700 Subject: [PATCH 40/81] Replace sleep with kubectl wait in prepare-supervisor-on-kind.sh - Now that the FederationDomain has `status.conditions`, we can use `kubectl wait` to wait for it to be ready in this hack script --- hack/prepare-supervisor-on-kind.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index e8c937d06..0696e17ba 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -351,8 +351,7 @@ spec: EOF echo "Waiting for FederationDomain to initialize or update..." -# Sleeping is a race, but that's probably good enough for the purposes of this script. -sleep 5 +kubectl wait --for=condition=Ready FederationDomain/my-federation-domain -n "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" # Test that the federation domain is working before we proceed. echo "Fetching FederationDomain discovery info via command: https_proxy=\"$PINNIPED_TEST_PROXY\" curl -fLsS --cacert \"$root_ca_crt_path\" \"$issuer/.well-known/openid-configuration\"" From 0aacedf943b479c3975b6bc7e07e6b4bf0a94cdc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 Jul 2023 09:09:46 -0700 Subject: [PATCH 41/81] Update proposal doc statuses --- proposals/1113_ldap-ad-web-ui/README.md | 2 +- proposals/1125_dynamic-supervisor-oidc-clients/README.md | 2 +- proposals/1141_audit-logging/README.md | 2 +- proposals/1406_multiple-idps/README.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 090a3c543..f892dc194 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -1,7 +1,7 @@ --- title: "Web UI for LDAP/AD login" authors: [ "@margocrawf" ] -status: "accepted" +status: "implemented" approval_date: "May 11, 2022" --- diff --git a/proposals/1125_dynamic-supervisor-oidc-clients/README.md b/proposals/1125_dynamic-supervisor-oidc-clients/README.md index c75b37046..dcb5dd869 100644 --- a/proposals/1125_dynamic-supervisor-oidc-clients/README.md +++ b/proposals/1125_dynamic-supervisor-oidc-clients/README.md @@ -1,7 +1,7 @@ --- title: "Dynamic Supervisor OIDC Clients" authors: [ "@cfryanr", "@enj" ] -status: "approved" +status: "implemented" sponsor: [ ] approval_date: "Jul 26, 2022" --- diff --git a/proposals/1141_audit-logging/README.md b/proposals/1141_audit-logging/README.md index 994b996c0..aaf1d54b4 100644 --- a/proposals/1141_audit-logging/README.md +++ b/proposals/1141_audit-logging/README.md @@ -1,7 +1,7 @@ --- title: "Audit Logging" authors: [ "@cfryanr" ] -status: "in-review" +status: "accepted" sponsor: [ ] approval_date: "" --- diff --git a/proposals/1406_multiple-idps/README.md b/proposals/1406_multiple-idps/README.md index d109f2638..11b973a92 100644 --- a/proposals/1406_multiple-idps/README.md +++ b/proposals/1406_multiple-idps/README.md @@ -1,9 +1,9 @@ --- title: "Multiple Identity Providers" authors: [ "@cfryanr" ] -status: "draft" +status: "accepted" sponsor: [] -approval_date: "" +approval_date: "July 12, 2023" --- *Disclaimer*: Proposals are point-in-time designs and decisions. Once approved and implemented, they become historical From 617f57e1c96733027e7a0da90004b0fd2c98d234 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 Jul 2023 15:26:32 -0700 Subject: [PATCH 42/81] Validate transforms const names in federation_domain_watcher.go --- .../federation_domain_watcher.go | 45 ++++++- .../federation_domain_watcher_test.go | 121 ++++++++++++++++++ test/integration/supervisor_discovery_test.go | 4 + 3 files changed, 165 insertions(+), 5 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 81e438005..e83b3f6e8 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -43,6 +43,7 @@ const ( typeIdentityProvidersDisplayNamesUnique = "IdentityProvidersDisplayNamesUnique" typeIdentityProvidersAPIGroupSuffixValid = "IdentityProvidersObjectRefAPIGroupSuffixValid" typeIdentityProvidersObjectRefKindValid = "IdentityProvidersObjectRefKindValid" + typeTransformsConstantsNamesUnique = "TransformsConstantsNamesUnique" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -57,6 +58,7 @@ const ( reasonDuplicateDisplayNames = "DuplicateDisplayNames" reasonAPIGroupNameUnrecognized = "APIGroupUnrecognized" reasonKindUnrecognized = "KindUnrecognized" + reasonDuplicateConstantsNames = "DuplicateConstantsNames" kindLDAPIdentityProvider = "LDAPIdentityProvider" kindOIDCIdentityProvider = "OIDCIdentityProvider" @@ -318,6 +320,7 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( conditions = appendIdentityProviderDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, []string{}, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) + conditions = appendTransformsConstantsNamesUniqueCondition(sets.Set[string]{}, conditions) return federationDomainIssuer, conditions, nil } @@ -374,7 +377,9 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic idpNotFoundIndices = append(idpNotFoundIndices, index) } - pipeline, err := c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name) + var err error + var pipeline *idtransform.TransformationPipeline + pipeline, conditions, err = c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name, conditions) if err != nil { return nil, nil, err } @@ -455,15 +460,24 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( idp configv1alpha1.FederationDomainIdentityProvider, federationDomainName string, -) (*idtransform.TransformationPipeline, error) { + conditions []*configv1alpha1.Condition, +) (*idtransform.TransformationPipeline, []*configv1alpha1.Condition, error) { pipeline := idtransform.NewTransformationPipeline() consts := &celtransformer.TransformationConstants{ StringConstants: map[string]string{}, StringListConstants: map[string][]string{}, } + constNames := sets.Set[string]{} + duplicateConstNames := sets.Set[string]{} // Read all the declared constants. for _, constant := range idp.Transforms.Constants { + // The CRD requires the name field, and validates that it has at least one character, + // so here we only need to validate that they are unique. + if constNames.Has(constant.Name) { + duplicateConstNames.Insert(constant.Name) + } + constNames.Insert(constant.Name) switch constant.Type { case "string": consts.StringConstants[constant.Name] = constant.StringValue @@ -471,9 +485,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit consts.StringListConstants[constant.Name] = constant.StringListValue default: // This shouldn't really happen since the CRD validates it, but handle it as an error. - return nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) + return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) } } + conditions = appendTransformsConstantsNamesUniqueCondition(duplicateConstNames, conditions) // Compile all the expressions and add them to the pipeline. for idx, expr := range idp.Transforms.Expressions { @@ -490,7 +505,7 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit } default: // This shouldn't really happen since the CRD validates it, but handle it as an error. - return nil, fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) + return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) } compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) if err != nil { @@ -580,7 +595,7 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit } } - return pipeline, nil + return pipeline, conditions, nil } func (c *federationDomainWatcherController) sortedAllowedKinds() []string { @@ -647,6 +662,26 @@ func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames return conditions } +func appendTransformsConstantsNamesUniqueCondition(duplicateConstNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if duplicateConstNames.Len() > 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsConstantsNamesUnique, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDuplicateConstantsNames, + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].transforms.constants[].name contain duplicates: %s", + strings.Join(sortAndQuote(duplicateConstNames.UnsortedList()), ", ")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsConstantsNamesUnique, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", + }) + } + return conditions +} + func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if err != nil { // Note that the FederationDomainIssuer constructors only validate the Issuer URL, diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 8cad14e11..70339f9b9 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -397,6 +397,28 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyConstNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsConstantsNamesUnique", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", + } + } + + sadConstNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsConstantsNamesUnique", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "DuplicateConstantsNames", + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].transforms.constants[].name contain duplicates: %s", duplicateNames), + } + } + happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", @@ -454,6 +476,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -467,6 +490,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -598,6 +622,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -644,6 +669,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -688,6 +714,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -704,6 +731,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -769,6 +797,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -785,6 +814,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -801,6 +831,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -831,6 +862,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -844,6 +876,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -869,6 +902,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -925,6 +959,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -1037,6 +1072,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: ldapIdentityProvider.Name, }, }, + { + DisplayName: "duplicate1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + }, { DisplayName: "unique", ObjectRef: corev1.TypedLocalObjectReference{ @@ -1073,6 +1116,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), @@ -1139,6 +1183,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -1201,6 +1246,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, configv1alpha1.FederationDomainPhaseError, sortConditionsByType([]configv1alpha1.Condition{ + happyConstNamesUniqueCondition(frozenMetav1Now, 123), sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -1215,6 +1261,81 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { })), }, }, + { + name: "the federation domain has duplicate transformation const names", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + { + Name: "duplicate1", + Type: "string", + StringValue: "abc", + }, + { + Name: "duplicate1", + Type: "string", + StringValue: "def", + }, + { + Name: "duplicate1", + Type: "string", + StringValue: "efg", + }, + { + Name: "duplicate2", + Type: "string", + StringValue: "123", + }, + { + Name: "duplicate2", + Type: "string", + StringValue: "456", + }, + { + Name: "unique", + Type: "string", + StringValue: "hij", + }, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + sortConditionsByType([]configv1alpha1.Condition{ + sadConstNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), + happyKindCondition(frozenMetav1Now, 123), + happyAPIGroupSuffixCondition(frozenMetav1Now, 123), + happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), + happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), + happyIssuerIsUniqueCondition(frozenMetav1Now, 123), + happyIssuerURLValidCondition(frozenMetav1Now, 123), + happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + })), + }, + }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 9e2b7807e..5f7a93adb 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -141,6 +141,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, + "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, }) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, @@ -151,6 +152,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, + "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) @@ -178,6 +180,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, + "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) @@ -694,6 +697,7 @@ func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interfa "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, + "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, }) } From be973bc87eb00cd6cebb67c684429e81316a412c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 Jul 2023 15:45:46 -0700 Subject: [PATCH 43/81] Allow for slower CI workers in celformer_test.go --- internal/celtransformer/celformer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 0adf9fa73..d781dae05 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -798,7 +798,7 @@ func TestTransformer(t *testing.T) { func TestTypicalPerformanceAndThreadSafety(t *testing.T) { t.Parallel() - transformer, err := NewCELTransformer(time.Second) // CI workers can be slow, so allow slow transforms + transformer, err := NewCELTransformer(5 * time.Second) // CI workers can be slow, so allow slow transforms require.NoError(t, err) pipeline := idtransform.NewTransformationPipeline() From 013030041ac07cd96001a8840c1592538e16dfd6 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Fri, 14 Jul 2023 13:27:25 -0400 Subject: [PATCH 44/81] Add helper for happy/sad conditions to federation_domain_watcher_test.go Co-authored-by: Ryan Richard --- .../federation_domain_watcher_test.go | 297 +++++++----------- 1 file changed, 121 insertions(+), 176 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 70339f9b9..0fbf7fa5c 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -502,6 +502,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }) } + replaceConditions := func(conditions []configv1alpha1.Condition, sadConditions []configv1alpha1.Condition) []configv1alpha1.Condition { + for _, sadReplaceCondition := range sadConditions { + for origIndex, origCondition := range conditions { + if origCondition.Type == sadReplaceCondition.Type { + conditions[origIndex] = sadReplaceCondition + break + } + } + } + return conditions + } + invalidIssuerURL := ":/host//path" _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. require.Error(t, err) @@ -621,17 +633,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, @@ -668,17 +675,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseReady, @@ -713,34 +715,24 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte.cOm/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - sadIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ @@ -796,51 +788,38 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte-adDress.cOm/path1", oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate-address.com:1234/path2", oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(oidcIdentityProvider.Name, frozenMetav1Now, 123), - unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), - sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), - unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(invalidIssuerURL, oidcIdentityProvider.Name, frozenMetav1Now, 123), + []configv1alpha1.Condition{ + unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), + unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate( &configv1alpha1.FederationDomain{ @@ -861,31 +840,21 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), expectedFederationDomainStatusUpdate(federationDomain2, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), }, }, @@ -901,17 +870,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantStatusUpdates: []*configv1alpha1.FederationDomain{ expectedFederationDomainStatusUpdate(federationDomain1, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), }, }, @@ -958,21 +922,16 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[0] with displayName "cant-find-me", `+ - `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ - `.spec.identityProviders[2] with displayName "cant-find-me-still"`, - frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[0] with displayName "cant-find-me", `+ + `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ + `.spec.identityProviders[2] with displayName "cant-find-me-still"`, + frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), ), }, }, @@ -1115,17 +1074,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - })), + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), }, }, { @@ -1182,21 +1137,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[0] with displayName "name1", `+ - `.spec.identityProviders[1] with displayName "name2", `+ - `.spec.identityProviders[2] with displayName "name3"`, - frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - })), + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[0] with displayName "name1", `+ + `.spec.identityProviders[1] with displayName "name2", `+ + `.spec.identityProviders[2] with displayName "name3"`, + frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), }, }, { @@ -1245,20 +1197,17 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[1] with displayName "name2", `+ - `.spec.identityProviders[2] with displayName "name3"`, - frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - })), + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[1] with displayName "name2", `+ + `.spec.identityProviders[2] with displayName "name3"`, + frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), }, }, { @@ -1323,17 +1272,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, }, configv1alpha1.FederationDomainPhaseError, - sortConditionsByType([]configv1alpha1.Condition{ - sadConstNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123), - happyIssuerIsUniqueCondition(frozenMetav1Now, 123), - happyIssuerURLValidCondition(frozenMetav1Now, 123), - happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - })), + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadConstNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), }, }, { From 52925a2a469770f3c8c3039a11b7e271d4994496 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 14 Jul 2023 12:06:20 -0700 Subject: [PATCH 45/81] Validate transforms expressions in federation_domain_watcher.go --- .../federation_domain_watcher.go | 168 +++++++++++++----- .../federation_domain_watcher_test.go | 160 +++++++++++------ test/integration/supervisor_discovery_test.go | 4 + 3 files changed, 237 insertions(+), 95 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index e83b3f6e8..1546f9128 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -44,6 +44,7 @@ const ( typeIdentityProvidersAPIGroupSuffixValid = "IdentityProvidersObjectRefAPIGroupSuffixValid" typeIdentityProvidersObjectRefKindValid = "IdentityProvidersObjectRefKindValid" typeTransformsConstantsNamesUnique = "TransformsConstantsNamesUnique" + typeTransformsExpressionsValid = "TransformsExpressionsValid" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -59,6 +60,7 @@ const ( reasonAPIGroupNameUnrecognized = "APIGroupUnrecognized" reasonKindUnrecognized = "KindUnrecognized" reasonDuplicateConstantsNames = "DuplicateConstantsNames" + reasonInvalidTransformsExpressions = "InvalidTransformsExpressions" kindLDAPIdentityProvider = "LDAPIdentityProvider" kindOIDCIdentityProvider = "OIDCIdentityProvider" @@ -162,7 +164,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } // Process each FederationDomain to validate its spec and to turn it into a FederationDomainIssuer. - federationDomainIssuers, fdToConditionsMap, err := c.processAllFederationDomains(federationDomains) + federationDomainIssuers, fdToConditionsMap, err := c.processAllFederationDomains(ctx.Context, federationDomains) if err != nil { return err } @@ -185,6 +187,7 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro } func (c *federationDomainWatcherController) processAllFederationDomains( + ctx context.Context, federationDomains []*configv1alpha1.FederationDomain, ) ([]*federationdomainproviders.FederationDomainIssuer, map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition, error) { federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) @@ -196,7 +199,7 @@ func (c *federationDomainWatcherController) processAllFederationDomains( conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) - federationDomainIssuer, conditions, err := c.makeFederationDomainIssuer(federationDomain, conditions) + federationDomainIssuer, conditions, err := c.makeFederationDomainIssuer(ctx, federationDomain, conditions) if err != nil { return nil, nil, err } @@ -216,6 +219,7 @@ func (c *federationDomainWatcherController) processAllFederationDomains( } func (c *federationDomainWatcherController) makeFederationDomainIssuer( + ctx context.Context, federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition, ) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { @@ -230,7 +234,7 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuer( return nil, nil, err } } else { - federationDomainIssuer, conditions, err = c.makeFederationDomainIssuerWithExplicitIDPs(federationDomain, conditions) + federationDomainIssuer, conditions, err = c.makeFederationDomainIssuerWithExplicitIDPs(ctx, federationDomain, conditions) if err != nil { return nil, nil, err } @@ -313,19 +317,23 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( }) } - // This is the constructor for the backwards compatibility mode. + // This is the constructor for the legacy backwards compatibility mode. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(federationDomain.Spec.Issuer, defaultFederationDomainIdentityProvider) conditions = appendIssuerURLValidCondition(err, conditions) + // These conditions can only have errors when the list of IDPs is explicitly configured, + // and in this case there are no IDPs explicitly configured, so set these conditions all to have no errors. conditions = appendIdentityProviderDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, []string{}, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) conditions = appendTransformsConstantsNamesUniqueCondition(sets.Set[string]{}, conditions) + conditions = appendTransformsExpressionsValidCondition([]string{}, conditions) return federationDomainIssuer, conditions, nil } func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplicitIDPs( + ctx context.Context, federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition, ) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { @@ -337,10 +345,13 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic badKinds := []string{} for index, idp := range federationDomain.Spec.IdentityProviders { + idpIsValid := true + // The CRD requires the displayName field, and validates that it has at least one character, // so here we only need to validate that they are unique. if displayNames.Has(idp.DisplayName) { duplicateDisplayNames.Insert(idp.DisplayName) + idpIsValid = false } displayNames.Insert(idp.DisplayName) @@ -361,6 +372,7 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic canTryToFindIDP = false } + // When the apiGroup and kind are valid, try to find the IDP CR. var idpResourceUID types.UID idpWasFound := false if canTryToFindIDP { @@ -375,26 +387,31 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic } if !canTryToFindIDP || !idpWasFound { idpNotFoundIndices = append(idpNotFoundIndices, index) + idpIsValid = false } var err error var pipeline *idtransform.TransformationPipeline - pipeline, conditions, err = c.makeTransformationPipelineForIdentityProvider(idp, federationDomain.Name, conditions) + var allExamplesPassed bool + pipeline, allExamplesPassed, conditions, err = c.makeTransformationPipelineAndEvaluateExamplesForIdentityProvider(ctx, idp, index, conditions) if err != nil { return nil, nil, err } + if !allExamplesPassed { + idpIsValid = false + } + + if !idpIsValid { + // Something about the IDP was not valid. Don't add it. + continue + } - // For each valid IDP (unique displayName, valid objectRef + valid transforms), add it to the list. + // For a valid IDP (unique displayName, valid objectRef, valid transforms), add it to the list. federationDomainIdentityProviders = append(federationDomainIdentityProviders, &federationdomainproviders.FederationDomainIdentityProvider{ DisplayName: idp.DisplayName, UID: idpResourceUID, Transforms: pipeline, }) - plog.Debug("loaded FederationDomain identity provider", - "federationDomain", federationDomain.Name, - "identityProviderDisplayName", idp.DisplayName, - "identityProviderResourceUID", idpResourceUID, - ) } if len(idpNotFoundIndices) != 0 { @@ -457,12 +474,31 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor return idpResourceUID, true, nil } -func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( +func (c *federationDomainWatcherController) makeTransformationPipelineAndEvaluateExamplesForIdentityProvider( + ctx context.Context, idp configv1alpha1.FederationDomainIdentityProvider, - federationDomainName string, + idpIndex int, conditions []*configv1alpha1.Condition, -) (*idtransform.TransformationPipeline, []*configv1alpha1.Condition, error) { - pipeline := idtransform.NewTransformationPipeline() +) (*idtransform.TransformationPipeline, bool, []*configv1alpha1.Condition, error) { + consts, conditions, err := c.makeTransformsConstants(idp, conditions) + if err != nil { + return nil, false, nil, err + } + + pipeline, conditions, err := c.makeTransformationPipeline(idp, idpIndex, consts, conditions) + if err != nil { + return nil, false, nil, err + } + + allExamplesPassed, conditions := c.evaluateExamples(ctx, idp, pipeline, conditions) + + return pipeline, allExamplesPassed, conditions, nil +} + +func (c *federationDomainWatcherController) makeTransformsConstants( + idp configv1alpha1.FederationDomainIdentityProvider, + conditions []*configv1alpha1.Condition, +) (*celtransformer.TransformationConstants, []*configv1alpha1.Condition, error) { consts := &celtransformer.TransformationConstants{ StringConstants: map[string]string{}, StringListConstants: map[string][]string{}, @@ -488,10 +524,23 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) } } + conditions = appendTransformsConstantsNamesUniqueCondition(duplicateConstNames, conditions) + return consts, conditions, nil +} + +func (c *federationDomainWatcherController) makeTransformationPipeline( + idp configv1alpha1.FederationDomainIdentityProvider, + idpIndex int, + consts *celtransformer.TransformationConstants, + conditions []*configv1alpha1.Condition, +) (*idtransform.TransformationPipeline, []*configv1alpha1.Condition, error) { + pipeline := idtransform.NewTransformationPipeline() + expressionsCompileErrors := []string{} + // Compile all the expressions and add them to the pipeline. - for idx, expr := range idp.Transforms.Expressions { + for exprIndex, expr := range idp.Transforms.Expressions { var rawTransform celtransformer.CELTransformation switch expr.Type { case "username/v1": @@ -507,37 +556,50 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit // This shouldn't really happen since the CRD validates it, but handle it as an error. return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) } + compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) if err != nil { - // TODO: handle compile err - plog.Error("error compiling identity transformation", err, - "federationDomain", federationDomainName, - "idpDisplayName", idp.DisplayName, - "transformationIndex", idx, - "transformationType", expr.Type, - "transformationExpression", expr.Expression, - ) + expressionsCompileErrors = append(expressionsCompileErrors, + fmt.Sprintf("spec.identityProvider[%d].transforms.expressions[%d].expression was invalid:\n%s", + idpIndex, exprIndex, err.Error())) } + pipeline.AppendTransformation(compiledTransform) - plog.Debug("successfully compiled identity transformation expression", - "type", expr.Type, - "expr", expr.Expression, - "policyMessage", expr.Message, - ) + } + + conditions = appendTransformsExpressionsValidCondition(expressionsCompileErrors, conditions) + + if len(expressionsCompileErrors) > 0 { + // One or more of the expressions did not compile, so we don't have a useful pipeline to return. + return nil, conditions, nil + } + + return pipeline, conditions, nil +} + +func (c *federationDomainWatcherController) evaluateExamples( + ctx context.Context, + idp configv1alpha1.FederationDomainIdentityProvider, + pipeline *idtransform.TransformationPipeline, + conditions []*configv1alpha1.Condition, +) (bool, []*configv1alpha1.Condition) { + if pipeline == nil { + // TODO cannot evaluate examples, but still need to write a condition for it + return false, conditions } // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. - for idx, e := range idp.Transforms.Examples { - // TODO: use a real context param below - result, _ := pipeline.Evaluate(context.TODO(), e.Username, e.Groups) + examplesErrors := []string{} + for examplesIndex, e := range idp.Transforms.Examples { + result, _ := pipeline.Evaluate(ctx, e.Username, e.Groups) // TODO: handle err resultWasAuthRejected := !result.AuthenticationAllowed if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif // TODO: handle this failed example + examplesErrors = append(examplesErrors, "TODO") plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", - "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, + "exampleIndex", examplesIndex, "expectedRejected", e.Expects.Rejected, "actualRejectedResult", resultWasAuthRejected, "expectedMessage", e.Expects.Message, @@ -545,10 +607,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit ) } else if !e.Expects.Rejected && resultWasAuthRejected { // TODO: handle this failed example + examplesErrors = append(examplesErrors, "TODO") plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", - "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, + "exampleIndex", examplesIndex, "expectedRejected", e.Expects.Rejected, "actualRejectedResult", resultWasAuthRejected, "expectedMessage", e.Expects.Message, @@ -557,10 +619,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { // TODO: when expected message is blank, then treat it like it expects the default message // TODO: handle this failed example + examplesErrors = append(examplesErrors, "TODO") plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", - "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, + "exampleIndex", examplesIndex, "expectedRejected", e.Expects.Rejected, "actualRejectedResult", resultWasAuthRejected, "expectedMessage", e.Expects.Message, @@ -572,10 +634,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit // TODO: when both of these fail, put both errors onto the status (not just the first one) if e.Expects.Username != result.Username { // TODO: handle this failed example + examplesErrors = append(examplesErrors, "TODO") plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", - "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, + "exampleIndex", examplesIndex, "expectedUsername", e.Expects.Username, "actualUsernameResult", result.Username, ) @@ -584,10 +646,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? // TODO: handle this failed example + examplesErrors = append(examplesErrors, "TODO") plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", - "federationDomain", federationDomainName, "idpDisplayName", idp.DisplayName, - "exampleIndex", idx, + "exampleIndex", examplesIndex, "expectedGroups", e.Expects.Groups, "actualGroupsResult", result.Groups, ) @@ -595,7 +657,7 @@ func (c *federationDomainWatcherController) makeTransformationPipelineForIdentit } } - return pipeline, conditions, nil + return len(examplesErrors) == 0, conditions } func (c *federationDomainWatcherController) sortedAllowedKinds() []string { @@ -642,6 +704,26 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s return conditions } +func appendTransformsExpressionsValidCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(errors) > 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsExpressionsValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonInvalidTransformsExpressions, + Message: fmt.Sprintf("the expressions specified by .spec.identityProviders[].transforms.expressions[] were invalid:\n\n%s", + strings.Join(errors, "\n\n")), + }) + } else { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsExpressionsValid, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", + }) + } + return conditions +} + func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if duplicateDisplayNames.Len() > 0 { conditions = append(conditions, &configv1alpha1.Condition{ diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 0fbf7fa5c..15031ba2c 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -27,6 +27,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" + "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/testutil" ) @@ -419,6 +420,28 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsExpressionsValid", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", + } + } + + sadTransformationExpressionsCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsExpressionsValid", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "InvalidTransformsExpressions", + Message: fmt.Sprintf("the expressions specified by .spec.identityProviders[].transforms.expressions[] were invalid:\n\n%s", errorMessages), + } + } + happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", @@ -474,22 +497,21 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { return cp } - allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { - return sortConditionsByType([]configv1alpha1.Condition{ - happyConstNamesUniqueCondition(frozenMetav1Now, 123), - happyKindCondition(frozenMetav1Now, 123), - happyAPIGroupSuffixCondition(frozenMetav1Now, 123), - happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), - happyIssuerIsUniqueCondition(time, observedGeneration), - happyIssuerURLValidCondition(time, observedGeneration), - happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), - happyReadyCondition(issuer, time, observedGeneration), - }) + replaceConditions := func(conditions []configv1alpha1.Condition, sadConditions []configv1alpha1.Condition) []configv1alpha1.Condition { + for _, sadReplaceCondition := range sadConditions { + for origIndex, origCondition := range conditions { + if origCondition.Type == sadReplaceCondition.Type { + conditions[origIndex] = sadReplaceCondition + break + } + } + } + return conditions } allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return sortConditionsByType([]configv1alpha1.Condition{ + happyTransformationExpressionsCondition(frozenMetav1Now, 123), happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), @@ -502,16 +524,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }) } - replaceConditions := func(conditions []configv1alpha1.Condition, sadConditions []configv1alpha1.Condition) []configv1alpha1.Condition { - for _, sadReplaceCondition := range sadConditions { - for origIndex, origCondition := range conditions { - if origCondition.Type == sadReplaceCondition.Type { - conditions[origIndex] = sadReplaceCondition - break - } - } - } - return conditions + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + return replaceConditions( + allHappyConditionsSuccess(issuer, time, observedGeneration), + []configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), + }, + ) } invalidIssuerURL := ":/host//path" @@ -1228,36 +1247,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ - { - Name: "duplicate1", - Type: "string", - StringValue: "abc", - }, - { - Name: "duplicate1", - Type: "string", - StringValue: "def", - }, - { - Name: "duplicate1", - Type: "string", - StringValue: "efg", - }, - { - Name: "duplicate2", - Type: "string", - StringValue: "123", - }, - { - Name: "duplicate2", - Type: "string", - StringValue: "456", - }, - { - Name: "unique", - Type: "string", - StringValue: "hij", - }, + {Name: "duplicate1", Type: "string", StringValue: "abc"}, + {Name: "duplicate1", Type: "string", StringValue: "def"}, + {Name: "duplicate1", Type: "string", StringValue: "efg"}, + {Name: "duplicate2", Type: "string", StringValue: "123"}, + {Name: "duplicate2", Type: "string", StringValue: "456"}, + {Name: "uniqueName", Type: "string", StringValue: "hij"}, }, }, }, @@ -1281,6 +1276,67 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "the federation domain has transformation expressions which don't compile", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: "this is not a valid cel expression"}, + {Type: "groups/v1", Expression: "this is also not a valid cel expression"}, + {Type: "username/v1", Expression: "username"}, // valid + {Type: "policy/v1", Expression: "still not a valid cel expression"}, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadTransformationExpressionsCondition( + here.Doc( + `spec.identityProvider[0].transforms.expressions[0].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is also not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting + | still not a valid cel expression + | ......^`), + frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), + }, + }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 5f7a93adb..ed2e2e204 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -142,6 +142,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, + "TransformsExpressionsValid": v1alpha1.ConditionTrue, }) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, @@ -153,6 +154,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, + "TransformsExpressionsValid": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) @@ -181,6 +183,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, + "TransformsExpressionsValid": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) @@ -698,6 +701,7 @@ func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interfa "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, + "TransformsExpressionsValid": v1alpha1.ConditionTrue, }) } From c771328bb1d142ac40fade876fcfa7b300f8a445 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 14 Jul 2023 16:50:43 -0700 Subject: [PATCH 46/81] Validate transforms examples in federation_domain_watcher.go Also changes the transformation pipeline code to sort and uniq the transformed group names at the end of the pipeline. This makes the results more predicable without changing the semantics. --- internal/celtransformer/celformer.go | 8 +- internal/celtransformer/celformer_test.go | 14 +- .../federation_domain_watcher.go | 161 +++++----- .../federation_domain_watcher_test.go | 277 +++++++++++++++++- .../idtransform/identity_transformations.go | 14 + .../identity_transformations_test.go | 31 +- test/integration/supervisor_discovery_test.go | 4 + 7 files changed, 427 insertions(+), 82 deletions(-) diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go index 2c1bf409d..bdf351b48 100644 --- a/internal/celtransformer/celformer.go +++ b/internal/celtransformer/celformer.go @@ -29,7 +29,7 @@ const ( constStringVariableName = "strConst" constStringListVariableName = "strListConst" - defaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy" + DefaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy" ) // CELTransformer can compile any number of transformation expression pipelines. @@ -96,6 +96,10 @@ type CELTransformation interface { compile(transformer *CELTransformer, consts *TransformationConstants) (idtransform.IdentityTransformation, error) } +var _ CELTransformation = (*UsernameTransformation)(nil) +var _ CELTransformation = (*GroupsTransformation)(nil) +var _ CELTransformation = (*AllowAuthenticationPolicy)(nil) + // UsernameTransformation is a CEL expression that can transform a username (or leave it unchanged). // It implements CELTransformation. type UsernameTransformation struct { @@ -290,7 +294,7 @@ func (c *compiledAllowAuthenticationPolicy) Evaluate(ctx context.Context, userna } if !boolValue { if len(c.rejectedAuthenticationMessage) == 0 { - result.RejectedAuthenticationMessage = defaultPolicyRejectedAuthMessage + result.RejectedAuthenticationMessage = DefaultPolicyRejectedAuthMessage } else { result.RejectedAuthenticationMessage = c.rejectedAuthenticationMessage } diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index d781dae05..227ed3928 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "runtime" + "sort" "sync" "testing" "time" @@ -113,7 +114,7 @@ func TestTransformer(t *testing.T) { &GroupsTransformation{Expression: `groups + [username + "2"]`}, // by the time this expression runs, the username was already changed to "other" }, wantUsername: "other", - wantGroups: []string{"admins", "developers", "other", "ryan", "other2"}, + wantGroups: []string{"admins", "developers", "other", "other2", "ryan"}, }, { name: "any transformation can use the provided constants as variables", @@ -135,7 +136,7 @@ func TestTransformer(t *testing.T) { &AllowAuthenticationPolicy{Expression: `strConst.x == "abc"`}, }, wantUsername: "abcuvw", - wantGroups: []string{"abc", "def", "xyz", "123"}, + wantGroups: []string{"123", "abc", "def", "xyz"}, }, { name: "the CEL string extensions are enabled for use in the expressions", @@ -297,7 +298,7 @@ func TestTransformer(t *testing.T) { &GroupsTransformation{Expression: `groups + ["new-group"]`}, }, wantUsername: "ryan", - wantGroups: []string{"admins", "developers", "other", "new-group"}, + wantGroups: []string{"admins", "developers", "new-group", "other"}, }, { name: "a nil passed as groups will be converted to an empty list", @@ -340,7 +341,7 @@ func TestTransformer(t *testing.T) { &GroupsTransformation{Expression: `groups + [strConst.groupToAlwaysAdd]`}, }, wantUsername: "ryan", - wantGroups: []string{"admins", "developers", "other", "new-group"}, + wantGroups: []string{"admins", "developers", "new-group", "other"}, }, { name: "can add a group but only if they already belong to another group - when the user does belong to that other group", @@ -350,7 +351,7 @@ func TestTransformer(t *testing.T) { &GroupsTransformation{Expression: `"other" in groups ? groups + ["new-group"] : groups`}, }, wantUsername: "ryan", - wantGroups: []string{"admins", "developers", "other", "new-group"}, + wantGroups: []string{"admins", "developers", "new-group", "other"}, }, { name: "can add a group but only if they already belong to another group - when the user does NOT belong to that other group", @@ -424,7 +425,7 @@ func TestTransformer(t *testing.T) { &AllowAuthenticationPolicy{Expression: `["foobar", "foobaz", "foobat"].all(g, g in groups)`, RejectedAuthenticationMessage: `Only users who belong to all groups in a list are allowed`}, }, wantUsername: "ryan", - wantGroups: []string{"admins", "developers", "other", "foobar", "foobaz", "foobat"}, + wantGroups: []string{"admins", "developers", "foobar", "foobat", "foobaz", "other"}, }, { name: "can reject auth unless the user belongs to all of the groups in a list - when the user does NOT meet the criteria", @@ -820,6 +821,7 @@ func TestTypicalPerformanceAndThreadSafety(t *testing.T) { groups = append(groups, fmt.Sprintf("g%d", i)) wantGroups = append(wantGroups, fmt.Sprintf("group_prefix:g%d", i)) } + sort.Strings(wantGroups) // Before looking at performance, check that the behavior of the function is correct. result, err := pipeline.Evaluate(context.Background(), "ryan", groups) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 1546f9128..4d10953d3 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -45,6 +45,7 @@ const ( typeIdentityProvidersObjectRefKindValid = "IdentityProvidersObjectRefKindValid" typeTransformsConstantsNamesUnique = "TransformsConstantsNamesUnique" typeTransformsExpressionsValid = "TransformsExpressionsValid" + typeTransformsExamplesPassed = "TransformsExamplesPassed" reasonSuccess = "Success" reasonNotReady = "NotReady" @@ -61,6 +62,7 @@ const ( reasonKindUnrecognized = "KindUnrecognized" reasonDuplicateConstantsNames = "DuplicateConstantsNames" reasonInvalidTransformsExpressions = "InvalidTransformsExpressions" + reasonTransformsExamplesFailed = "TransformsExamplesFailed" kindLDAPIdentityProvider = "LDAPIdentityProvider" kindOIDCIdentityProvider = "OIDCIdentityProvider" @@ -328,10 +330,12 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) conditions = appendTransformsConstantsNamesUniqueCondition(sets.Set[string]{}, conditions) conditions = appendTransformsExpressionsValidCondition([]string{}, conditions) + conditions = appendTransformsExamplesPassedCondition([]string{}, conditions) return federationDomainIssuer, conditions, nil } +//nolint:funlen func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplicitIDPs( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, @@ -490,7 +494,7 @@ func (c *federationDomainWatcherController) makeTransformationPipelineAndEvaluat return nil, false, nil, err } - allExamplesPassed, conditions := c.evaluateExamples(ctx, idp, pipeline, conditions) + allExamplesPassed, conditions := c.evaluateExamples(ctx, idp, idpIndex, pipeline, conditions) return pipeline, allExamplesPassed, conditions, nil } @@ -580,83 +584,76 @@ func (c *federationDomainWatcherController) makeTransformationPipeline( func (c *federationDomainWatcherController) evaluateExamples( ctx context.Context, idp configv1alpha1.FederationDomainIdentityProvider, + idpIndex int, pipeline *idtransform.TransformationPipeline, conditions []*configv1alpha1.Condition, ) (bool, []*configv1alpha1.Condition) { + const errorFmt = ".spec.identityProviders[%d].transforms.examples[%d] example failed:\nexpected: %s\nactual: %s" + examplesErrors := []string{} + if pipeline == nil { - // TODO cannot evaluate examples, but still need to write a condition for it + // Unable to evaluate the conditions where the pipeline of expressions was invalid. + conditions = appendTransformsExamplesPassedCondition(nil, conditions) return false, conditions } // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. - examplesErrors := []string{} - for examplesIndex, e := range idp.Transforms.Examples { - result, _ := pipeline.Evaluate(ctx, e.Username, e.Groups) - // TODO: handle err + for exIndex, e := range idp.Transforms.Examples { + result, err := pipeline.Evaluate(ctx, e.Username, e.Groups) + if err != nil { + examplesErrors = append(examplesErrors, fmt.Sprintf(errorFmt, idpIndex, exIndex, + "no transformation errors", + fmt.Sprintf("transformations resulted in an unexpected error %q", err.Error()))) + continue + } resultWasAuthRejected := !result.AuthenticationAllowed - if e.Expects.Rejected && !resultWasAuthRejected { //nolint:gocritic,nestif - // TODO: handle this failed example - examplesErrors = append(examplesErrors, "TODO") - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication to be rejected but it was not", - "idpDisplayName", idp.DisplayName, - "exampleIndex", examplesIndex, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if !e.Expects.Rejected && resultWasAuthRejected { - // TODO: handle this failed example - examplesErrors = append(examplesErrors, "TODO") - plog.Warning("FederationDomain identity provider transformations example failed: expected authentication not to be rejected but it was rejected", - "idpDisplayName", idp.DisplayName, - "exampleIndex", examplesIndex, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if e.Expects.Rejected && resultWasAuthRejected && e.Expects.Message != result.RejectedAuthenticationMessage { - // TODO: when expected message is blank, then treat it like it expects the default message - // TODO: handle this failed example - examplesErrors = append(examplesErrors, "TODO") - plog.Warning("FederationDomain identity provider transformations example failed: expected a different authentication rejection message", - "idpDisplayName", idp.DisplayName, - "exampleIndex", examplesIndex, - "expectedRejected", e.Expects.Rejected, - "actualRejectedResult", resultWasAuthRejected, - "expectedMessage", e.Expects.Message, - "actualMessageResult", result.RejectedAuthenticationMessage, - ) - } else if result.AuthenticationAllowed { + + if e.Expects.Rejected && !resultWasAuthRejected { + examplesErrors = append(examplesErrors, + fmt.Sprintf(errorFmt, idpIndex, exIndex, "authentication to be rejected", "authentication was not rejected")) + continue + } + + if !e.Expects.Rejected && resultWasAuthRejected { + examplesErrors = append(examplesErrors, fmt.Sprintf(errorFmt, idpIndex, exIndex, + "authentication not to be rejected", + fmt.Sprintf("authentication was rejected with message %q", result.RejectedAuthenticationMessage))) + continue + } + + expectedRejectionMessage := e.Expects.Message + if len(expectedRejectionMessage) == 0 { + expectedRejectionMessage = celtransformer.DefaultPolicyRejectedAuthMessage + } + if e.Expects.Rejected && resultWasAuthRejected && expectedRejectionMessage != result.RejectedAuthenticationMessage { + examplesErrors = append(examplesErrors, fmt.Sprintf(errorFmt, idpIndex, exIndex, + fmt.Sprintf("authentication rejection message %q", expectedRejectionMessage), + fmt.Sprintf("authentication rejection message %q", result.RejectedAuthenticationMessage))) + continue + } + + if result.AuthenticationAllowed { // In the case where the user expected the auth to be allowed and it was allowed, then compare // the expected username and group names to the actual username and group names. - // TODO: when both of these fail, put both errors onto the status (not just the first one) if e.Expects.Username != result.Username { - // TODO: handle this failed example - examplesErrors = append(examplesErrors, "TODO") - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed username", - "idpDisplayName", idp.DisplayName, - "exampleIndex", examplesIndex, - "expectedUsername", e.Expects.Username, - "actualUsernameResult", result.Username, - ) + examplesErrors = append(examplesErrors, fmt.Sprintf(errorFmt, idpIndex, exIndex, + fmt.Sprintf("username %q", e.Expects.Username), + fmt.Sprintf("username %q", result.Username))) + } + expectedGroups := e.Expects.Groups + if expectedGroups == nil { + expectedGroups = []string{} } - if !stringSlicesEqual(e.Expects.Groups, result.Groups) { - // TODO: Do we need to make this insensitive to ordering, or should the transformations evaluator be changed to always return sorted group names at the end of the pipeline? - // TODO: What happens if the user did not write any group expectation? Treat it like expecting an empty list of groups? - // TODO: handle this failed example - examplesErrors = append(examplesErrors, "TODO") - plog.Warning("FederationDomain identity provider transformations example failed: expected a different transformed groups list", - "idpDisplayName", idp.DisplayName, - "exampleIndex", examplesIndex, - "expectedGroups", e.Expects.Groups, - "actualGroupsResult", result.Groups, - ) + if !stringSetsEqual(expectedGroups, result.Groups) { + examplesErrors = append(examplesErrors, fmt.Sprintf(errorFmt, idpIndex, exIndex, + fmt.Sprintf("groups [%s]", strings.Join(sortAndQuote(expectedGroups), ", ")), + fmt.Sprintf("groups [%s]", strings.Join(sortAndQuote(result.Groups), ", ")))) } } } + conditions = appendTransformsExamplesPassedCondition(examplesErrors, conditions) + return len(examplesErrors) == 0, conditions } @@ -724,6 +721,34 @@ func appendTransformsExpressionsValidCondition(errors []string, conditions []*co return conditions } +func appendTransformsExamplesPassedCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + switch { + case errors == nil: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsExamplesPassed, + Status: configv1alpha1.ConditionUnknown, + Reason: reasonUnableToValidate, + Message: "unable to check if the examples specified by .spec.identityProviders[].transforms.examples[] had errors because an expression was invalid", + }) + case len(errors) > 0: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsExamplesPassed, + Status: configv1alpha1.ConditionFalse, + Reason: reasonTransformsExamplesFailed, + Message: fmt.Sprintf("the examples specified by .spec.identityProviders[].transforms.examples[] had errors:\n\n%s", + strings.Join(errors, "\n\n")), + }) + default: + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeTransformsExamplesPassed, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", + }) + } + return conditions +} + func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if duplicateDisplayNames.Len() > 0 { conditions = append(conditions, &configv1alpha1.Condition{ @@ -948,14 +973,8 @@ func hadErrorCondition(conditions []*configv1alpha1.Condition) bool { return false } -func stringSlicesEqual(a []string, b []string) bool { - if len(a) != len(b) { - return false - } - for i, itemFromA := range a { - if b[i] != itemFromA { - return false - } - } - return true +func stringSetsEqual(a []string, b []string) bool { + aSet := sets.New(a...) + bSet := sets.New(b...) + return aSet.Equal(bSet) } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 15031ba2c..2ff07cd39 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -442,6 +442,39 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } + happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsExamplesPassed", + Status: "True", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "Success", + Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", + } + } + + sadTransformationExamplesCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsExamplesPassed", + Status: "False", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "TransformsExamplesFailed", + Message: fmt.Sprintf("the examples specified by .spec.identityProviders[].transforms.examples[] had errors:\n\n%s", errorMessages), + } + } + + unknownTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "TransformsExamplesPassed", + Status: "Unknown", + ObservedGeneration: observedGeneration, + LastTransitionTime: time, + Reason: "UnableToValidate", + Message: "unable to check if the examples specified by .spec.identityProviders[].transforms.examples[] had errors because an expression was invalid", + } + } + happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", @@ -511,6 +544,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { return sortConditionsByType([]configv1alpha1.Condition{ + happyTransformationExamplesCondition(frozenMetav1Now, 123), happyTransformationExpressionsCondition(frozenMetav1Now, 123), happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), @@ -1330,7 +1364,248 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { spec.identityProvider[0].transforms.expressions[3].expression was invalid: CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting | still not a valid cel expression - | ......^`), + | ......^`, + ), + frozenMetav1Now, 123), + unknownTransformationExamplesCondition(frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), + }, + }, + { + name: "the federation domain has transformation examples which don't pass", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "policy/v1", Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, Message: "only ryan allowed"}, + {Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified + {Type: "username/v1", Expression: `"pre:" + username`}, + {Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"pre:b", "pre:a", "pre:b", "pre:a"}, // order and repeats don't matter, treated like a set + Rejected: false, + }, + }, + { // this example should pass + Username: "other", + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only ryan allowed", + }, + }, + { // this example should fail because it expects the user to be rejected but the user was actually not rejected + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "this input is ignored in this case", + }, + }, + { // this example should fail because it expects the user not to be rejected but they were actually rejected + Username: "other", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:other", + Groups: []string{"pre:a", "pre:b"}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong rejection message + Username: "other", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "wrong message", + }, + }, + { // this example should pass even though it does not make any assertion about the rejection message + // because the message assertions defaults to asserting the default rejection message + Username: "rejectMeWithDefaultMessage", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + }, + }, + { // this example should fail because it expects both the wrong username and groups + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong username only + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{"pre:b", "pre:a"}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong groups only + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"wrong2", "wrong1"}, + Rejected: false, + }, + }, + { // this example should fail because it does not expect anything but the auth actually was successful + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, + }, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadTransformationExamplesCondition( + here.Doc( + `.spec.identityProviders[0].transforms.examples[2] example failed: + expected: authentication to be rejected + actual: authentication was not rejected + + .spec.identityProviders[0].transforms.examples[3] example failed: + expected: authentication not to be rejected + actual: authentication was rejected with message "only ryan allowed" + + .spec.identityProviders[0].transforms.examples[4] example failed: + expected: authentication rejection message "wrong message" + actual: authentication rejection message "only ryan allowed" + + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[0].transforms.examples[7] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[8] example failed: + expected: groups ["wrong1", "wrong2"] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: username "" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"]`, + ), + frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), + }, + }, + { + name: "the federation domain has transformation expressions that return illegal values with examples which exercise them", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `username == "ryan" ? "" : username`}, // not allowed to return an empty string as the transformed username + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { // every example which encounters an unexpected error should fail because the transformation pipeline returned an error + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, + }, + { // every example which encounters an unexpected error should fail because the transformation pipeline returned an error + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{}, + }, + { // this should pass + Username: "other", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "other", + Groups: []string{"a", "b"}, + Rejected: false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + replaceConditions( + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadTransformationExamplesCondition( + here.Doc( + `.spec.identityProviders[0].transforms.examples[0] example failed: + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" + + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, + ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), diff --git a/internal/idtransform/identity_transformations.go b/internal/idtransform/identity_transformations.go index 095b98f81..133d2601b 100644 --- a/internal/idtransform/identity_transformations.go +++ b/internal/idtransform/identity_transformations.go @@ -8,7 +8,10 @@ package idtransform import ( "context" "fmt" + "sort" "strings" + + "k8s.io/apimachinery/pkg/util/sets" ) // TransformationResult is the result of evaluating a transformation against some inputs. @@ -50,11 +53,13 @@ func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, if groups == nil { groups = []string{} } + accumulatedResult := &TransformationResult{ Username: username, Groups: groups, AuthenticationAllowed: true, } + for i, transform := range p.transforms { var err error accumulatedResult, err = transform.Evaluate(ctx, accumulatedResult.Username, accumulatedResult.Groups) @@ -73,6 +78,15 @@ func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, return nil, fmt.Errorf("identity transformation returned a null list of groups, which is not allowed") } } + + accumulatedResult.Groups = sortAndUniq(accumulatedResult.Groups) + // There were no unexpected errors and no policy which rejected auth. return accumulatedResult, nil } + +func sortAndUniq(s []string) []string { + unique := sets.New(s...).UnsortedList() + sort.Strings(unique) + return unique +} diff --git a/internal/idtransform/identity_transformations_test.go b/internal/idtransform/identity_transformations_test.go index a19b6febb..f3e8707fa 100644 --- a/internal/idtransform/identity_transformations_test.go +++ b/internal/idtransform/identity_transformations_test.go @@ -110,6 +110,29 @@ func TestTransformationPipeline(t *testing.T) { wantAuthenticationAllowed: true, wantRejectionAuthenticationMessage: "none", }, + { + name: "group results are sorted and made unique", + transforms: []IdentityTransformation{ + FakeAppendStringTransformer{}, + }, + username: "foo", + groups: []string{ + "b", + "a", + "b", + "a", + "c", + "b", + }, + wantUsername: "foo:transformed", + wantGroups: []string{ + "a:transformed", + "b:transformed", + "c:transformed", + }, + wantAuthenticationAllowed: true, + wantRejectionAuthenticationMessage: "none", + }, { name: "multiple transformations applied successfully", username: "foo", @@ -163,7 +186,9 @@ func TestTransformationPipeline(t *testing.T) { { name: "unexpected error at index", username: "foo", - groups: []string{"foobar"}, + groups: []string{ + "foobar", + }, transforms: []IdentityTransformation{ FakeAppendStringTransformer{}, FakeErrorTransformer{}, @@ -214,7 +239,9 @@ func TestTransformationPipeline(t *testing.T) { { name: "any transformation returning nil for the list of groups will cause an error", username: "foo", - groups: []string{"these.will.be.converted.to.nil"}, + groups: []string{ + "these.will.be.converted.to.nil", + }, transforms: []IdentityTransformation{ FakeNilGroupTransformer{}, }, diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index ed2e2e204..4e8d061ca 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -143,6 +143,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, + "TransformsExamplesPassed": v1alpha1.ConditionTrue, }) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionFalse, @@ -155,6 +156,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, + "TransformsExamplesPassed": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) @@ -184,6 +186,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, + "TransformsExamplesPassed": v1alpha1.ConditionTrue, }) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) @@ -702,6 +705,7 @@ func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interfa "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, + "TransformsExamplesPassed": v1alpha1.ConditionTrue, }) } From b89e6d9d933902f9a2b59a45415503a98cc2697f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 17 Jul 2023 16:41:28 -0700 Subject: [PATCH 47/81] Make it possible to compare transformation pipelines in unit tests --- internal/celtransformer/celformer.go | 21 ++ internal/celtransformer/celformer_test.go | 12 + .../federation_domain_watcher_test.go | 291 +++++++++++++++++- .../idtransform/identity_transformations.go | 12 + .../identity_transformations_test.go | 109 +++++-- 5 files changed, 414 insertions(+), 31 deletions(-) diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go index bdf351b48..085af4502 100644 --- a/internal/celtransformer/celformer.go +++ b/internal/celtransformer/celformer.go @@ -160,6 +160,7 @@ func (t *UsernameTransformation) compile(transformer *CELTransformer, consts *Tr baseCompiledTransformation: &baseCompiledTransformation{ program: program, consts: consts, + sourceExpr: t, maxExpressionRuntime: transformer.maxExpressionRuntime, }, }, nil @@ -174,6 +175,7 @@ func (t *GroupsTransformation) compile(transformer *CELTransformer, consts *Tran baseCompiledTransformation: &baseCompiledTransformation{ program: program, consts: consts, + sourceExpr: t, maxExpressionRuntime: transformer.maxExpressionRuntime, }, }, nil @@ -188,6 +190,7 @@ func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer, consts baseCompiledTransformation: &baseCompiledTransformation{ program: program, consts: consts, + sourceExpr: t, maxExpressionRuntime: transformer.maxExpressionRuntime, }, rejectedAuthenticationMessage: t.RejectedAuthenticationMessage, @@ -198,6 +201,7 @@ func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer, consts type baseCompiledTransformation struct { program cel.Program consts *TransformationConstants + sourceExpr CELTransformation maxExpressionRuntime time.Duration } @@ -302,6 +306,23 @@ func (c *compiledAllowAuthenticationPolicy) Evaluate(ctx context.Context, userna return result, nil } +type CELTransformationSource struct { + Expr CELTransformation + Consts *TransformationConstants +} + +func (c *compiledUsernameTransformation) Source() interface{} { + return &CELTransformationSource{Expr: c.sourceExpr, Consts: c.consts} +} + +func (c *compiledGroupsTransformation) Source() interface{} { + return &CELTransformationSource{Expr: c.sourceExpr, Consts: c.consts} +} + +func (c *compiledAllowAuthenticationPolicy) Source() interface{} { + return &CELTransformationSource{Expr: c.sourceExpr, Consts: c.consts} +} + func newEnv() (*cel.Env, error) { // Note that Kubernetes uses CEL in several places, which are helpful to see as an example of // how to configure the CEL compiler for production usage. Examples: diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 227ed3928..a38a1b236 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -765,6 +765,7 @@ func TestTransformer(t *testing.T) { require.NoError(t, err) pipeline := idtransform.NewTransformationPipeline() + expectedPipelineSource := []interface{}{} for _, transform := range tt.transforms { compiledTransform, err := transformer.CompileTransformation(transform, tt.consts) @@ -774,6 +775,15 @@ func TestTransformer(t *testing.T) { } require.NoError(t, err, "got an unexpected compile error") pipeline.AppendTransformation(compiledTransform) + + expectedTransformSource := &CELTransformationSource{ + Expr: transform, + Consts: tt.consts, + } + if expectedTransformSource.Consts == nil { + expectedTransformSource.Consts = &TransformationConstants{} + } + expectedPipelineSource = append(expectedPipelineSource, expectedTransformSource) } ctx := context.Background() @@ -792,6 +802,8 @@ func TestTransformer(t *testing.T) { require.Equal(t, tt.wantGroups, result.Groups) require.Equal(t, !tt.wantAuthRejected, result.AuthenticationAllowed, "AuthenticationAllowed had unexpected value") require.Equal(t, tt.wantAuthRejectedMessage, result.RejectedAuthenticationMessage) + + require.Equal(t, expectedPipelineSource, pipeline.Source()) }) } } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 2ff07cd39..93d9f8ac4 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -17,6 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" "k8s.io/utils/pointer" @@ -25,6 +26,7 @@ import ( idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/celtransformer" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/here" @@ -1282,7 +1284,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ {Name: "duplicate1", Type: "string", StringValue: "abc"}, - {Name: "duplicate1", Type: "string", StringValue: "def"}, + {Name: "duplicate1", Type: "stringList", StringListValue: []string{"def"}}, {Name: "duplicate1", Type: "string", StringValue: "efg"}, {Name: "duplicate2", Type: "string", StringValue: "123"}, {Name: "duplicate2", Type: "string", StringValue: "456"}, @@ -1612,6 +1614,133 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "the federation domain has valid IDPs and transformations and examples", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + ldapIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://issuer1.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "policy/v1", Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, Message: "only ryan allowed"}, + {Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified + {Type: "username/v1", Expression: `"pre:" + username`}, + {Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`}, + }, + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + {Name: "str", Type: "string", StringValue: "abc"}, + {Name: "strL", Type: "stringList", StringListValue: []string{"def"}}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"pre:b", "pre:a"}, + Rejected: false, + }, + }, + { + Username: "other", + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only ryan allowed", + }, + }, + { + Username: "rejectMeWithDefaultMessage", + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + // Not specifying message is the same as expecting the default message. + }, + }, + { + Username: "rejectMeWithDefaultMessage", + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "Authentication was rejected by a configured policy", // this is the default message + }, + }, + }, + }, + }, + { + DisplayName: "name2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "LDAPIdentityProvider", + Name: ldapIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `"pre:" + username`}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"b", "a"}, + Rejected: false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithIDPs(t, "https://issuer1.com", []*federationdomainproviders.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + UID: oidcIdentityProvider.UID, + Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{ + StringConstants: map[string]string{"str": "abc"}, + StringListConstants: map[string][]string{"strL": {"def"}}, + }, + &celtransformer.AllowAuthenticationPolicy{ + Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, + RejectedAuthenticationMessage: "only ryan allowed", + }, + &celtransformer.AllowAuthenticationPolicy{Expression: `username != "rejectMeWithDefaultMessage"`}, + &celtransformer.UsernameTransformation{Expression: `"pre:" + username`}, + &celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`}, + ), + }, + { + DisplayName: "name2", + UID: ldapIdentityProvider.UID, + Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{}, + &celtransformer.UsernameTransformation{Expression: `"pre:" + username`}, + ), + }, + }), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), + ), + }, + }, { name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it", inputObjects: []runtime.Object{ @@ -1719,7 +1848,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { if tt.wantFDIssuers != nil { require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) - require.ElementsMatch(t, tt.wantFDIssuers, federationDomainsSetter.FederationDomainsReceived) + // This is ugly, but we cannot test equality on compiled identity transformations because cel.Program + // cannot be compared for equality. This converts them to a type which can be tested for equality, + // which should be good enough for the purposes of this test. + require.ElementsMatch(t, + convertToComparableType(tt.wantFDIssuers), + convertToComparableType(federationDomainsSetter.FederationDomainsReceived)) } else { require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) } @@ -1743,6 +1877,46 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } +type comparableFederationDomainIssuer struct { + issuer string + identityProviders []*comparableFederationDomainIdentityProvider + defaultIdentityProvider *comparableFederationDomainIdentityProvider +} + +type comparableFederationDomainIdentityProvider struct { + DisplayName string + UID types.UID + TransformsSource []interface{} +} + +func makeFederationDomainIdentityProviderComparable(fdi *federationdomainproviders.FederationDomainIdentityProvider) *comparableFederationDomainIdentityProvider { + if fdi == nil { + return nil + } + return &comparableFederationDomainIdentityProvider{ + DisplayName: fdi.DisplayName, + UID: fdi.UID, + TransformsSource: fdi.Transforms.Source(), + } +} + +func convertToComparableType(fdis []*federationdomainproviders.FederationDomainIssuer) []*comparableFederationDomainIssuer { + result := []*comparableFederationDomainIssuer{} + for _, fdi := range fdis { + comparableFDIs := make([]*comparableFederationDomainIdentityProvider, len(fdi.IdentityProviders())) + for _, idp := range fdi.IdentityProviders() { + comparableFDIs = append(comparableFDIs, makeFederationDomainIdentityProviderComparable(idp)) + } + converted := &comparableFederationDomainIssuer{ + issuer: fdi.Issuer(), + identityProviders: comparableFDIs, + defaultIdentityProvider: makeFederationDomainIdentityProviderComparable(fdi.DefaultIdentityProvider()), + } + result = append(result, converted) + } + return result +} + func expectedFederationDomainStatusUpdate( fd *configv1alpha1.FederationDomain, phase configv1alpha1.FederationDomainPhase, @@ -1789,3 +1963,116 @@ func sortFederationDomainsByName(federationDomains []*configv1alpha1.FederationD return federationDomains[a].GetName() < federationDomains[b].GetName() }) } + +func newTransformationPipeline( + t *testing.T, + consts *celtransformer.TransformationConstants, + transformations ...celtransformer.CELTransformation, +) *idtransform.TransformationPipeline { + pipeline := idtransform.NewTransformationPipeline() + + compiler, err := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime) + require.NoError(t, err) + + if consts.StringConstants == nil { + consts.StringConstants = map[string]string{} + } + if consts.StringListConstants == nil { + consts.StringListConstants = map[string][]string{} + } + + for _, transform := range transformations { + compiledTransform, err := compiler.CompileTransformation(transform, consts) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform) + } + + return pipeline +} + +func TestTransformationPipelinesCanBeTestedForEqualityUsingSourceToMakeTestingEasier(t *testing.T) { + compiler, err := celtransformer.NewCELTransformer(5 * time.Second) + require.NoError(t, err) + + transforms := []celtransformer.CELTransformation{ + &celtransformer.AllowAuthenticationPolicy{ + Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, + RejectedAuthenticationMessage: "only ryan allowed", + }, + &celtransformer.UsernameTransformation{Expression: `"pre:" + username`}, + &celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`}, + } + + differentTransforms := []celtransformer.CELTransformation{ + &celtransformer.AllowAuthenticationPolicy{ + Expression: `username == "ryan" || username == "different"`, + RejectedAuthenticationMessage: "different", + }, + &celtransformer.UsernameTransformation{Expression: `"different:" + username`}, + &celtransformer.GroupsTransformation{Expression: `groups.map(g, "different:" + g)`}, + } + + consts := &celtransformer.TransformationConstants{ + StringConstants: map[string]string{ + "foo": "bar", + "baz": "bat", + }, + StringListConstants: map[string][]string{ + "foo": {"a", "b"}, + "bar": {"c", "d"}, + }, + } + + differentConsts := &celtransformer.TransformationConstants{ + StringConstants: map[string]string{ + "foo": "barDifferent", + "baz": "bat", + }, + StringListConstants: map[string][]string{ + "foo": {"aDifferent", "b"}, + "bar": {"c", "d"}, + }, + } + + pipeline := idtransform.NewTransformationPipeline() + equalPipeline := idtransform.NewTransformationPipeline() + differentPipeline1 := idtransform.NewTransformationPipeline() + differentPipeline2 := idtransform.NewTransformationPipeline() + expectedSourceList := []interface{}{} + + for i, transform := range transforms { + // Compile and append to a pipeline. + compiledTransform1, err := compiler.CompileTransformation(transform, consts) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform1) + + // Recompile the same thing and append it to another pipeline. + // This pipeline should end up being equal to the first one. + compiledTransform2, err := compiler.CompileTransformation(transform, consts) + require.NoError(t, err) + equalPipeline.AppendTransformation(compiledTransform2) + + // Build up a test expectation value. + expectedSourceList = append(expectedSourceList, &celtransformer.CELTransformationSource{Expr: transform, Consts: consts}) + + // Compile a different expression using the same constants and append it to a different pipeline. + // This should not be equal to the other pipelines. + compiledDifferentExpressionSameConsts, err := compiler.CompileTransformation(differentTransforms[i], consts) + require.NoError(t, err) + differentPipeline1.AppendTransformation(compiledDifferentExpressionSameConsts) + + // Compile the same expression using the different constants and append it to a different pipeline. + // This should not be equal to the other pipelines. + compiledSameExpressionDifferentConsts, err := compiler.CompileTransformation(transform, differentConsts) + require.NoError(t, err) + differentPipeline2.AppendTransformation(compiledSameExpressionDifferentConsts) + } + + require.Equal(t, expectedSourceList, pipeline.Source()) + require.Equal(t, expectedSourceList, equalPipeline.Source()) + + // The source of compiled pipelines can be compared to each other in this way for testing purposes. + require.Equal(t, pipeline.Source(), equalPipeline.Source()) + require.NotEqual(t, pipeline.Source(), differentPipeline1.Source()) + require.NotEqual(t, pipeline.Source(), differentPipeline2.Source()) +} diff --git a/internal/idtransform/identity_transformations.go b/internal/idtransform/identity_transformations.go index 133d2601b..2518b8e24 100644 --- a/internal/idtransform/identity_transformations.go +++ b/internal/idtransform/identity_transformations.go @@ -25,6 +25,10 @@ type TransformationResult struct { // IdentityTransformation is an individual identity transformation which can be evaluated. type IdentityTransformation interface { Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) + + // Source returns some representation of the original source code of the transformation, which is + // useful for tests to be able to check that a compiled transformation came from the right source. + Source() interface{} } // TransformationPipeline is a list of identity transforms, which can be evaluated in order against some given input @@ -85,6 +89,14 @@ func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, return accumulatedResult, nil } +func (p *TransformationPipeline) Source() []interface{} { + result := []interface{}{} + for _, transform := range p.transforms { + result = append(result, transform.Source()) + } + return result +} + func sortAndUniq(s []string) []string { unique := sets.New(s...).UnsortedList() sort.Strings(unique) diff --git a/internal/idtransform/identity_transformations_test.go b/internal/idtransform/identity_transformations_test.go index f3e8707fa..80d8f2df2 100644 --- a/internal/idtransform/identity_transformations_test.go +++ b/internal/idtransform/identity_transformations_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/require" ) -type FakeNoopTransformer struct{} +type fakeNoopTransformer struct{} -func (a FakeNoopTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeNoopTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { return &TransformationResult{ Username: username, Groups: groups, @@ -22,9 +22,13 @@ func (a FakeNoopTransformer) Evaluate(ctx context.Context, username string, grou }, nil } -type FakeNilGroupTransformer struct{} +func (a fakeNoopTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeNilGroupTransformer struct{} -func (a FakeNilGroupTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeNilGroupTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { return &TransformationResult{ Username: username, Groups: nil, @@ -33,9 +37,13 @@ func (a FakeNilGroupTransformer) Evaluate(ctx context.Context, username string, }, nil } -type FakeAppendStringTransformer struct{} +func (a fakeNilGroupTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeAppendStringTransformer struct{} -func (a FakeAppendStringTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeAppendStringTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { newGroups := []string{} for _, group := range groups { newGroups = append(newGroups, group+":transformed") @@ -48,9 +56,13 @@ func (a FakeAppendStringTransformer) Evaluate(ctx context.Context, username stri }, nil } -type FakeDeleteUsernameAndGroupsTransformer struct{} +func (a fakeAppendStringTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeDeleteUsernameAndGroupsTransformer struct{} -func (d FakeDeleteUsernameAndGroupsTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeDeleteUsernameAndGroupsTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { return &TransformationResult{ Username: "", Groups: []string{}, @@ -59,9 +71,13 @@ func (d FakeDeleteUsernameAndGroupsTransformer) Evaluate(ctx context.Context, us }, nil } -type FakeAuthenticationDisallowedTransformer struct{} +func (a fakeDeleteUsernameAndGroupsTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeAuthenticationDisallowedTransformer struct{} -func (d FakeAuthenticationDisallowedTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeAuthenticationDisallowedTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { newGroups := []string{} for _, group := range groups { newGroups = append(newGroups, group+":disallowed") @@ -74,13 +90,33 @@ func (d FakeAuthenticationDisallowedTransformer) Evaluate(ctx context.Context, u }, nil } -type FakeErrorTransformer struct{} +func (a fakeAuthenticationDisallowedTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeErrorTransformer struct{} -func (d FakeErrorTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { +func (a fakeErrorTransformer) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { return &TransformationResult{}, errors.New("unexpected catastrophic error") } -func TestTransformationPipeline(t *testing.T) { +func (a fakeErrorTransformer) Source() interface{} { + return nil // not needed for this test +} + +type fakeTransformerWithSource struct { + source string +} + +func (a fakeTransformerWithSource) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { + return nil, nil // not needed for this test +} + +func (a fakeTransformerWithSource) Source() interface{} { + return a.source +} + +func TestTransformationPipelineEvaluation(t *testing.T) { tests := []struct { name string username string @@ -95,7 +131,7 @@ func TestTransformationPipeline(t *testing.T) { { name: "single transformation applied successfully", transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, }, username: "foo", groups: []string{ @@ -113,7 +149,7 @@ func TestTransformationPipeline(t *testing.T) { { name: "group results are sorted and made unique", transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, }, username: "foo", groups: []string{ @@ -141,8 +177,8 @@ func TestTransformationPipeline(t *testing.T) { "foobaz", }, transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, }, wantUsername: "foo:transformed:transformed", wantGroups: []string{ @@ -159,7 +195,7 @@ func TestTransformationPipeline(t *testing.T) { "foobar", }, transforms: []IdentityTransformation{ - FakeAuthenticationDisallowedTransformer{}, + fakeAuthenticationDisallowedTransformer{}, }, wantUsername: "foo:disallowed", wantGroups: []string{"foobar:disallowed"}, @@ -173,10 +209,10 @@ func TestTransformationPipeline(t *testing.T) { "foobar", }, transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, - FakeAuthenticationDisallowedTransformer{}, + fakeAppendStringTransformer{}, + fakeAuthenticationDisallowedTransformer{}, // this transformation will not be run because the previous exits the pipeline - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, }, wantUsername: "foo:transformed:disallowed", wantGroups: []string{"foobar:transformed:disallowed"}, @@ -190,9 +226,9 @@ func TestTransformationPipeline(t *testing.T) { "foobar", }, transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, - FakeErrorTransformer{}, - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, + fakeErrorTransformer{}, + fakeAppendStringTransformer{}, }, wantError: "identity transformation at index 1: unexpected catastrophic error", }, @@ -200,7 +236,7 @@ func TestTransformationPipeline(t *testing.T) { name: "empty username not allowed", username: "foo", transforms: []IdentityTransformation{ - FakeDeleteUsernameAndGroupsTransformer{}, + fakeDeleteUsernameAndGroupsTransformer{}, }, wantError: "identity transformation returned an empty username, which is not allowed", }, @@ -208,7 +244,7 @@ func TestTransformationPipeline(t *testing.T) { name: "whitespace username not allowed", username: " \t\n\r ", transforms: []IdentityTransformation{ - FakeNoopTransformer{}, + fakeNoopTransformer{}, }, wantError: "identity transformation returned an empty username, which is not allowed", }, @@ -217,7 +253,7 @@ func TestTransformationPipeline(t *testing.T) { username: "foo", groups: []string{}, transforms: []IdentityTransformation{ - FakeAppendStringTransformer{}, + fakeAppendStringTransformer{}, }, wantUsername: "foo:transformed", wantGroups: []string{}, @@ -229,7 +265,7 @@ func TestTransformationPipeline(t *testing.T) { username: "foo", groups: nil, transforms: []IdentityTransformation{ - FakeNoopTransformer{}, + fakeNoopTransformer{}, }, wantUsername: "foo", wantGroups: []string{}, @@ -243,7 +279,7 @@ func TestTransformationPipeline(t *testing.T) { "these.will.be.converted.to.nil", }, transforms: []IdentityTransformation{ - FakeNilGroupTransformer{}, + fakeNilGroupTransformer{}, }, wantError: "identity transformation returned a null list of groups, which is not allowed", }, @@ -287,3 +323,18 @@ func TestTransformationPipeline(t *testing.T) { }) } } + +func TestTransformationSource(t *testing.T) { + pipeline := NewTransformationPipeline() + + for _, transform := range []IdentityTransformation{ + &fakeTransformerWithSource{source: "foo"}, + &fakeTransformerWithSource{source: "bar"}, + &fakeTransformerWithSource{source: "baz"}, + } { + pipeline.AppendTransformation(transform) + } + + require.Equal(t, []interface{}{"foo", "bar", "baz"}, pipeline.Source()) + require.NotEqual(t, []interface{}{"foo", "something-else", "baz"}, pipeline.Source()) +} From e42e3ca421453203b9aa3c5ccf8f457e0f664663 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 18 Jul 2023 15:00:58 -0700 Subject: [PATCH 48/81] Status condition messages for IDP transforms show index of invalid IDP --- .../federation_domain_watcher.go | 174 +++---- .../federation_domain_watcher_test.go | 449 ++++++++++++++---- 2 files changed, 460 insertions(+), 163 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 4d10953d3..e40f9c447 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -328,7 +328,7 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( conditions = appendIdentityProviderDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, []string{}, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) - conditions = appendTransformsConstantsNamesUniqueCondition(sets.Set[string]{}, conditions) + conditions = appendTransformsConstantsNamesUniqueCondition([]string{}, conditions) conditions = appendTransformsExpressionsValidCondition([]string{}, conditions) conditions = appendTransformsExamplesPassedCondition([]string{}, conditions) @@ -347,6 +347,7 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic duplicateDisplayNames := sets.Set[string]{} badAPIGroupNames := []string{} badKinds := []string{} + validationErrorMessages := &transformsValidationErrorMessages{} for index, idp := range federationDomain.Spec.IdentityProviders { idpIsValid := true @@ -397,7 +398,8 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic var err error var pipeline *idtransform.TransformationPipeline var allExamplesPassed bool - pipeline, allExamplesPassed, conditions, err = c.makeTransformationPipelineAndEvaluateExamplesForIdentityProvider(ctx, idp, index, conditions) + pipeline, allExamplesPassed, err = c.makeTransformationPipelineAndEvaluateExamplesForIdentityProvider( + ctx, idp, index, validationErrorMessages) if err != nil { return nil, nil, err } @@ -448,6 +450,10 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), badKinds, conditions) + conditions = appendTransformsConstantsNamesUniqueCondition(validationErrorMessages.errorsForConstants, conditions) + conditions = appendTransformsExpressionsValidCondition(validationErrorMessages.errorsForExpressions, conditions) + conditions = appendTransformsExamplesPassedCondition(validationErrorMessages.errorsForExamples, conditions) + return federationDomainIssuer, conditions, nil } func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { @@ -482,27 +488,36 @@ func (c *federationDomainWatcherController) makeTransformationPipelineAndEvaluat ctx context.Context, idp configv1alpha1.FederationDomainIdentityProvider, idpIndex int, - conditions []*configv1alpha1.Condition, -) (*idtransform.TransformationPipeline, bool, []*configv1alpha1.Condition, error) { - consts, conditions, err := c.makeTransformsConstants(idp, conditions) + validationErrorMessages *transformsValidationErrorMessages, +) (*idtransform.TransformationPipeline, bool, error) { + consts, errorsForConstants, err := c.makeTransformsConstantsForIdentityProvider(idp, idpIndex) if err != nil { - return nil, false, nil, err + return nil, false, err + } + if len(errorsForConstants) > 0 { + validationErrorMessages.errorsForConstants = append(validationErrorMessages.errorsForConstants, errorsForConstants) } - pipeline, conditions, err := c.makeTransformationPipeline(idp, idpIndex, consts, conditions) + pipeline, errorsForExpressions, err := c.makeTransformationPipelineForIdentityProvider(idp, idpIndex, consts) if err != nil { - return nil, false, nil, err + return nil, false, err + } + if len(errorsForExpressions) > 0 { + validationErrorMessages.errorsForExpressions = append(validationErrorMessages.errorsForExpressions, errorsForExpressions) } - allExamplesPassed, conditions := c.evaluateExamples(ctx, idp, idpIndex, pipeline, conditions) + allExamplesPassed, errorsForExamples := c.evaluateExamplesForIdentityProvider(ctx, idp, idpIndex, pipeline) + if len(errorsForExamples) > 0 { + validationErrorMessages.errorsForExamples = append(validationErrorMessages.errorsForExamples, errorsForExamples) + } - return pipeline, allExamplesPassed, conditions, nil + return pipeline, allExamplesPassed, nil } -func (c *federationDomainWatcherController) makeTransformsConstants( +func (c *federationDomainWatcherController) makeTransformsConstantsForIdentityProvider( idp configv1alpha1.FederationDomainIdentityProvider, - conditions []*configv1alpha1.Condition, -) (*celtransformer.TransformationConstants, []*configv1alpha1.Condition, error) { + idpIndex int, +) (*celtransformer.TransformationConstants, string, error) { consts := &celtransformer.TransformationConstants{ StringConstants: map[string]string{}, StringListConstants: map[string][]string{}, @@ -525,21 +540,24 @@ func (c *federationDomainWatcherController) makeTransformsConstants( consts.StringListConstants[constant.Name] = constant.StringListValue default: // This shouldn't really happen since the CRD validates it, but handle it as an error. - return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) + return nil, "", fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) } } - conditions = appendTransformsConstantsNamesUniqueCondition(duplicateConstNames, conditions) + if duplicateConstNames.Len() > 0 { + return consts, fmt.Sprintf( + "the names specified by .spec.identityProviders[%d].transforms.constants[].name contain duplicates: %s", + idpIndex, strings.Join(sortAndQuote(duplicateConstNames.UnsortedList()), ", ")), nil + } - return consts, conditions, nil + return consts, "", nil } -func (c *federationDomainWatcherController) makeTransformationPipeline( +func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( idp configv1alpha1.FederationDomainIdentityProvider, idpIndex int, consts *celtransformer.TransformationConstants, - conditions []*configv1alpha1.Condition, -) (*idtransform.TransformationPipeline, []*configv1alpha1.Condition, error) { +) (*idtransform.TransformationPipeline, string, error) { pipeline := idtransform.NewTransformationPipeline() expressionsCompileErrors := []string{} @@ -558,7 +576,7 @@ func (c *federationDomainWatcherController) makeTransformationPipeline( } default: // This shouldn't really happen since the CRD validates it, but handle it as an error. - return nil, nil, fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) + return nil, "", fmt.Errorf("one of spec.identityProvider[].transforms.expressions[].type is invalid: %q", expr.Type) } compiledTransform, err := c.celTransformer.CompileTransformation(rawTransform, consts) @@ -571,30 +589,29 @@ func (c *federationDomainWatcherController) makeTransformationPipeline( pipeline.AppendTransformation(compiledTransform) } - conditions = appendTransformsExpressionsValidCondition(expressionsCompileErrors, conditions) - if len(expressionsCompileErrors) > 0 { // One or more of the expressions did not compile, so we don't have a useful pipeline to return. - return nil, conditions, nil + // Return the validation messages. + return nil, strings.Join(expressionsCompileErrors, "\n\n"), nil } - return pipeline, conditions, nil + return pipeline, "", nil } -func (c *federationDomainWatcherController) evaluateExamples( +func (c *federationDomainWatcherController) evaluateExamplesForIdentityProvider( ctx context.Context, idp configv1alpha1.FederationDomainIdentityProvider, idpIndex int, pipeline *idtransform.TransformationPipeline, - conditions []*configv1alpha1.Condition, -) (bool, []*configv1alpha1.Condition) { +) (bool, string) { const errorFmt = ".spec.identityProviders[%d].transforms.examples[%d] example failed:\nexpected: %s\nactual: %s" examplesErrors := []string{} if pipeline == nil { // Unable to evaluate the conditions where the pipeline of expressions was invalid. - conditions = appendTransformsExamplesPassedCondition(nil, conditions) - return false, conditions + return false, fmt.Sprintf( + "unable to check if the examples specified by .spec.identityProviders[%d].transforms.examples[] had errors because an expression was invalid", + idpIndex) } // Run all the provided transform examples. If any fail, put errors on the FederationDomain status. @@ -652,13 +669,11 @@ func (c *federationDomainWatcherController) evaluateExamples( } } - conditions = appendTransformsExamplesPassedCondition(examplesErrors, conditions) - - return len(examplesErrors) == 0, conditions -} + if len(examplesErrors) > 0 { + return false, strings.Join(examplesErrors, "\n\n") + } -func (c *federationDomainWatcherController) sortedAllowedKinds() []string { - return sortAndQuote(c.allowedKinds.UnsortedList()) + return true, "" } func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { @@ -704,11 +719,10 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s func appendTransformsExpressionsValidCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if len(errors) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsExpressionsValid, - Status: configv1alpha1.ConditionFalse, - Reason: reasonInvalidTransformsExpressions, - Message: fmt.Sprintf("the expressions specified by .spec.identityProviders[].transforms.expressions[] were invalid:\n\n%s", - strings.Join(errors, "\n\n")), + Type: typeTransformsExpressionsValid, + Status: configv1alpha1.ConditionFalse, + Reason: reasonInvalidTransformsExpressions, + Message: strings.Join(errors, "\n\n"), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ @@ -722,23 +736,14 @@ func appendTransformsExpressionsValidCondition(errors []string, conditions []*co } func appendTransformsExamplesPassedCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - switch { - case errors == nil: + if len(errors) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeTransformsExamplesPassed, - Status: configv1alpha1.ConditionUnknown, - Reason: reasonUnableToValidate, - Message: "unable to check if the examples specified by .spec.identityProviders[].transforms.examples[] had errors because an expression was invalid", - }) - case len(errors) > 0: - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsExamplesPassed, - Status: configv1alpha1.ConditionFalse, - Reason: reasonTransformsExamplesFailed, - Message: fmt.Sprintf("the examples specified by .spec.identityProviders[].transforms.examples[] had errors:\n\n%s", - strings.Join(errors, "\n\n")), + Status: configv1alpha1.ConditionFalse, + Reason: reasonTransformsExamplesFailed, + Message: strings.Join(errors, "\n\n"), }) - default: + } else { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeTransformsExamplesPassed, Status: configv1alpha1.ConditionTrue, @@ -749,41 +754,40 @@ func appendTransformsExamplesPassedCondition(errors []string, conditions []*conf return conditions } -func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if duplicateDisplayNames.Len() > 0 { +func appendTransformsConstantsNamesUniqueCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(errors) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersDisplayNamesUnique, - Status: configv1alpha1.ConditionFalse, - Reason: reasonDuplicateDisplayNames, - Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", - strings.Join(sortAndQuote(duplicateDisplayNames.UnsortedList()), ", ")), + Type: typeTransformsConstantsNamesUnique, + Status: configv1alpha1.ConditionFalse, + Reason: reasonDuplicateConstantsNames, + Message: strings.Join(errors, "\n\n"), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersDisplayNamesUnique, + Type: typeTransformsConstantsNamesUnique, Status: configv1alpha1.ConditionTrue, Reason: reasonSuccess, - Message: "the names specified by .spec.identityProviders[].displayName are unique", + Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", }) } return conditions } -func appendTransformsConstantsNamesUniqueCondition(duplicateConstNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if duplicateConstNames.Len() > 0 { +func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if duplicateDisplayNames.Len() > 0 { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsConstantsNamesUnique, + Type: typeIdentityProvidersDisplayNamesUnique, Status: configv1alpha1.ConditionFalse, - Reason: reasonDuplicateConstantsNames, - Message: fmt.Sprintf("the names specified by .spec.identityProviders[].transforms.constants[].name contain duplicates: %s", - strings.Join(sortAndQuote(duplicateConstNames.UnsortedList()), ", ")), + Reason: reasonDuplicateDisplayNames, + Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", + strings.Join(sortAndQuote(duplicateDisplayNames.UnsortedList()), ", ")), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsConstantsNamesUnique, + Type: typeIdentityProvidersDisplayNamesUnique, Status: configv1alpha1.ConditionTrue, Reason: reasonSuccess, - Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", + Message: "the names specified by .spec.identityProviders[].displayName are unique", }) } return conditions @@ -810,15 +814,6 @@ func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condi return conditions } -func sortAndQuote(strs []string) []string { - quoted := make([]string, 0, len(strs)) - for _, s := range strs { - quoted = append(quoted, fmt.Sprintf("%q", s)) - } - sort.Strings(quoted) - return quoted -} - func (c *federationDomainWatcherController) updateStatus( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, @@ -859,6 +854,25 @@ func (c *federationDomainWatcherController) updateStatus( return err } +func sortAndQuote(strs []string) []string { + quoted := make([]string, 0, len(strs)) + for _, s := range strs { + quoted = append(quoted, fmt.Sprintf("%q", s)) + } + sort.Strings(quoted) + return quoted +} + +func (c *federationDomainWatcherController) sortedAllowedKinds() []string { + return sortAndQuote(c.allowedKinds.UnsortedList()) +} + +type transformsValidationErrorMessages struct { + errorsForConstants []string + errorsForExpressions []string + errorsForExamples []string +} + type crossFederationDomainConfigValidator struct { issuerCounts map[string]int uniqueSecretNamesPerIssuerAddress map[string]map[string]bool diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 93d9f8ac4..4bf20fafb 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -411,14 +411,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadConstNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + sadConstNamesUniqueCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsConstantsNamesUnique", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "DuplicateConstantsNames", - Message: fmt.Sprintf("the names specified by .spec.identityProviders[].transforms.constants[].name contain duplicates: %s", duplicateNames), + Message: errorMessages, } } @@ -440,7 +440,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "InvalidTransformsExpressions", - Message: fmt.Sprintf("the expressions specified by .spec.identityProviders[].transforms.expressions[] were invalid:\n\n%s", errorMessages), + Message: errorMessages, } } @@ -462,18 +462,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "TransformsExamplesFailed", - Message: fmt.Sprintf("the examples specified by .spec.identityProviders[].transforms.examples[] had errors:\n\n%s", errorMessages), - } - } - - unknownTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "TransformsExamplesPassed", - Status: "Unknown", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "UnableToValidate", - Message: "unable to check if the examples specified by .spec.identityProviders[].transforms.examples[] had errors because an expression was invalid", + Message: errorMessages, } } @@ -587,7 +576,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, }, { - name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " + + name: "legacy config: when no identity provider is specified on federation domains, but exactly one OIDC identity " + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + "matching the only identity provider found", inputObjects: []runtime.Object{ @@ -610,6 +599,54 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "legacy config: when no identity provider is specified on federation domains, but exactly one LDAP identity " + + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + + "matching the only identity provider found", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + ldapIdentityProvider, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, ldapIdentityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, ldapIdentityProvider.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123), + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123), + ), + }, + }, + { + name: "legacy config: when no identity provider is specified on federation domains, but exactly one AD identity " + + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + + "matching the only identity provider found", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + adIdentityProvider, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, adIdentityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, adIdentityProvider.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, adIdentityProvider.Name, frozenMetav1Now, 123), + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, adIdentityProvider.Name, frozenMetav1Now, 123), + ), + }, + }, { name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + "the out-of-date FederationDomain", @@ -637,6 +674,40 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "when the status of the FederationDomains is based on an old generation, it is updated", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer}, + Status: configv1alpha1.FederationDomainStatus{ + Phase: configv1alpha1.FederationDomainPhaseReady, + Conditions: allHappyConditionsLegacyConfigurationSuccess( + federationDomain1.Spec.Issuer, + oidcIdentityProvider.Name, + frozenMetav1Now, + 2, // this is an older generation + ), + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + // only one update, because the other FederationDomain already had the right status + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess( + federationDomain1.Spec.Issuer, + oidcIdentityProvider.Name, + frozenMetav1Now, + 123, // all conditions are updated to the new observed generation + ), + ), + }, + }, { name: "when there are two valid FederationDomains, but updating one fails, the status on the FederationDomain will not change", inputObjects: []runtime.Object{ @@ -1306,7 +1377,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ - sadConstNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), + sadConstNamesUniqueCondition( + `the names specified by .spec.identityProviders[0].transforms.constants[].name contain duplicates: "duplicate1", "duplicate2"`, + frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1351,25 +1424,25 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ - sadTransformationExpressionsCondition( - here.Doc( - `spec.identityProvider[0].transforms.expressions[0].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting - | this is not a valid cel expression - | .....^ - - spec.identityProvider[0].transforms.expressions[1].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting - | this is also not a valid cel expression - | .....^ - - spec.identityProvider[0].transforms.expressions[3].expression was invalid: - CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting - | still not a valid cel expression - | ......^`, - ), + sadTransformationExpressionsCondition(here.Doc( + `spec.identityProvider[0].transforms.expressions[0].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is also not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting + | still not a valid cel expression + | ......^`, + ), frozenMetav1Now, 123), + sadTransformationExamplesCondition( + "unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid", frozenMetav1Now, 123), - unknownTransformationExamplesCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1497,45 +1570,43 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ - sadTransformationExamplesCondition( - here.Doc( - `.spec.identityProviders[0].transforms.examples[2] example failed: - expected: authentication to be rejected - actual: authentication was not rejected - - .spec.identityProviders[0].transforms.examples[3] example failed: - expected: authentication not to be rejected - actual: authentication was rejected with message "only ryan allowed" - - .spec.identityProviders[0].transforms.examples[4] example failed: - expected: authentication rejection message "wrong message" - actual: authentication rejection message "only ryan allowed" - - .spec.identityProviders[0].transforms.examples[6] example failed: - expected: username "wrong" - actual: username "pre:ryan" - - .spec.identityProviders[0].transforms.examples[6] example failed: - expected: groups [] - actual: groups ["pre:a", "pre:b"] - - .spec.identityProviders[0].transforms.examples[7] example failed: - expected: username "wrong" - actual: username "pre:ryan" - - .spec.identityProviders[0].transforms.examples[8] example failed: - expected: groups ["wrong1", "wrong2"] - actual: groups ["pre:a", "pre:b"] - - .spec.identityProviders[0].transforms.examples[9] example failed: - expected: username "" - actual: username "pre:ryan" - - .spec.identityProviders[0].transforms.examples[9] example failed: - expected: groups [] - actual: groups ["pre:a", "pre:b"]`, - ), - frozenMetav1Now, 123), + sadTransformationExamplesCondition(here.Doc( + `.spec.identityProviders[0].transforms.examples[2] example failed: + expected: authentication to be rejected + actual: authentication was not rejected + + .spec.identityProviders[0].transforms.examples[3] example failed: + expected: authentication not to be rejected + actual: authentication was rejected with message "only ryan allowed" + + .spec.identityProviders[0].transforms.examples[4] example failed: + expected: authentication rejection message "wrong message" + actual: authentication rejection message "only ryan allowed" + + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[0].transforms.examples[7] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[8] example failed: + expected: groups ["wrong1", "wrong2"] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: username "" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"]`, + ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1598,16 +1669,228 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ + sadTransformationExamplesCondition(here.Doc( + `.spec.identityProviders[0].transforms.examples[0] example failed: + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" + + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, + ), frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), + }, + }, + { + name: "the federation domain has lots of errors including errors from multiple IDPs, which are all shown in the status conditions using IDP indices in the messages", + inputObjects: []runtime.Object{ + oidcIdentityProvider, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://not-unique.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "not unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: "this will not be found", + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string", StringValue: "bar"}, + {Name: "foo", Type: "string", StringValue: "baz"}, + }, + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `username + ":suffix"`}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { // this should fail + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "this is wrong string", + Groups: []string{"this is wrong string list"}, + }, + }, + { // this should fail + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "this is also wrong string", + Groups: []string{"this is also wrong string list"}, + }, + }, + }, + }, + }, + { + DisplayName: "not unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "this is wrong", + Name: "foo", + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string", StringValue: "bar"}, + {Name: "foo", Type: "string", StringValue: "baz"}, + }, + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `username + ":suffix"`}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { // this should pass + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "ryan:suffix", + Groups: []string{"a", "b"}, + }, + }, + { // this should fail + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: "this is still wrong string", + Groups: []string{"this is still wrong string list"}, + }, + }, + }, + }, + }, + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("this is wrong"), + Kind: "OIDCIdentityProvider", + Name: "foo", + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `username`}, + {Type: "username/v1", Expression: `this does not compile`}, + {Type: "username/v1", Expression: `username`}, + {Type: "username/v1", Expression: `this also does not compile`}, + }, + }, + }, + }, + }, + }, + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, + Spec: configv1alpha1.FederationDomainSpec{ + Issuer: "https://not-unique.com", + IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "name1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(apiGroupSupervisor), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: `username`}, + {Type: "username/v1", Expression: `this still does not compile`}, + {Type: "username/v1", Expression: `username`}, + {Type: "username/v1", Expression: `this really does not compile`}, + }, + }, + }, + }, + }, + }, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + replaceConditions( + allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadConstNamesUniqueCondition(here.Doc( + `the names specified by .spec.identityProviders[0].transforms.constants[].name contain duplicates: "foo" + + the names specified by .spec.identityProviders[1].transforms.constants[].name contain duplicates: "foo"`, + ), frozenMetav1Now, 123), + sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123), + sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( + `.spec.identityProviders[0] with displayName "not unique", .spec.identityProviders[1] with displayName "not unique", .spec.identityProviders[2] with displayName "name1"`, + frozenMetav1Now, 123), + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadKindCondition(`"this is wrong"`, frozenMetav1Now, 123), + sadTransformationExpressionsCondition(here.Doc( + `spec.identityProvider[2].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'does' expecting + | this does not compile + | .....^ + + spec.identityProvider[2].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'also' expecting + | this also does not compile + | .....^`, + ), frozenMetav1Now, 123), + sadTransformationExamplesCondition(here.Doc( + `.spec.identityProviders[0].transforms.examples[0] example failed: + expected: username "this is wrong string" + actual: username "ryan:suffix" + + .spec.identityProviders[0].transforms.examples[0] example failed: + expected: groups ["this is wrong string list"] + actual: groups ["a", "b"] + + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: username "this is also wrong string" + actual: username "ryan:suffix" + + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: groups ["this is also wrong string list"] + actual: groups ["a", "b"] + + .spec.identityProviders[1].transforms.examples[1] example failed: + expected: username "this is still wrong string" + actual: username "ryan:suffix" + + .spec.identityProviders[1].transforms.examples[1] example failed: + expected: groups ["this is still wrong string list"] + actual: groups ["a", "b"] + + unable to check if the examples specified by .spec.identityProviders[2].transforms.examples[] had errors because an expression was invalid`, + ), frozenMetav1Now, 123), + sadReadyCondition(frozenMetav1Now, 123), + }), + ), + expectedFederationDomainStatusUpdate( + &configv1alpha1.FederationDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, + }, + configv1alpha1.FederationDomainPhaseError, + replaceConditions( + allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123), + []configv1alpha1.Condition{ + sadIssuerIsUniqueCondition(frozenMetav1Now, 123), + sadTransformationExpressionsCondition(here.Doc( + `spec.identityProvider[0].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'still' expecting + | this still does not compile + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'really' expecting + | this really does not compile + | .....^`, + ), frozenMetav1Now, 123), sadTransformationExamplesCondition( - here.Doc( - `.spec.identityProviders[0].transforms.examples[0] example failed: - expected: no transformation errors - actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" - - .spec.identityProviders[0].transforms.examples[1] example failed: - expected: no transformation errors - actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, - ), + "unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid", frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), From 64f41d0d0cf21277d70974f69b9d460d7a72f424 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 18 Jul 2023 16:22:21 -0700 Subject: [PATCH 49/81] use multiple IDPs in manager_test.go --- .../endpointsmanager/manager_test.go | 153 ++++++++++++------ 1 file changed, 103 insertions(+), 50 deletions(-) diff --git a/internal/federationdomain/endpointsmanager/manager_test.go b/internal/federationdomain/endpointsmanager/manager_test.go index de6ea70b0..819aa3612 100644 --- a/internal/federationdomain/endpointsmanager/manager_test.go +++ b/internal/federationdomain/endpointsmanager/manager_test.go @@ -50,9 +50,14 @@ func TestManager(t *testing.T) { issuer2 = "https://example.com/some/path/more/deeply/nested/path" // note that this is a sub-path of the other issuer url issuer2DifferentCaseHostname = "https://exAmPlE.Com/some/path/more/deeply/nested/path" issuer2KeyID = "issuer2-key" - upstreamIDPAuthorizationURL = "https://test-upstream.com/auth" - upstreamIDPName = "test-idp" - upstreamResourceUID = "test-resource-uid" + upstreamIDPAuthorizationURL1 = "https://test-upstream.com/auth1" + upstreamIDPAuthorizationURL2 = "https://test-upstream.com/auth2" + upstreamIDPDisplayName1 = "test-idp-display-name-1" + upstreamIDPDisplayName2 = "test-idp-display-name-2" + upstreamIDPName1 = "test-idp-1" + upstreamIDPName2 = "test-idp-2" + upstreamResourceUID1 = "test-resource-uid-1" + upstreamResourceUID2 = "test-resource-uid-2" upstreamIDPType = "oidc" downstreamClientID = "pinniped-cli" downstreamRedirectURL = "http://127.0.0.1:12345/callback" @@ -82,7 +87,7 @@ func TestManager(t *testing.T) { r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right discovery endpoint was called - r.Equal(http.StatusOK, recorder.Code) + r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) parsedDiscoveryResult := discovery.Metadata{} @@ -92,7 +97,7 @@ func TestManager(t *testing.T) { r.Equal(parsedDiscoveryResult.SupervisorDiscovery.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPathV1Alpha1) } - requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIDPName, expectedIDPType string, expectedFlows []string) { + requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix string, expectedIDPNames []string, expectedIDPTypes string, expectedFlows []string) { recorder := httptest.NewRecorder() subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPathV1Alpha1+requestURLSuffix)) @@ -102,12 +107,18 @@ func TestManager(t *testing.T) { expectedFlowsJSON, err := json.Marshal(expectedFlows) require.NoError(t, err) + expectedIDPJSONList := []string{} + for i := range expectedIDPNames { + expectedIDPJSONList = append(expectedIDPJSONList, fmt.Sprintf(`{"name":"%s","type":"%s","flows":%s}`, + expectedIDPNames[i], expectedIDPTypes, expectedFlowsJSON)) + } + // Minimal check to ensure that the right IDP discovery endpoint was called - r.Equal(http.StatusOK, recorder.Code) + r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) r.Equal( - fmt.Sprintf(`{"pinniped_identity_providers":[{"name":"%s","type":"%s","flows":%s}]}`+"\n", expectedIDPName, expectedIDPType, expectedFlowsJSON), + fmt.Sprintf(`{"pinniped_identity_providers":[%s]}`+"\n", strings.Join(expectedIDPJSONList, ",")), string(responseBody), ) } @@ -120,7 +131,7 @@ func TestManager(t *testing.T) { r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right endpoint was called - r.Equal(http.StatusSeeOther, recorder.Code) + r.Equal(http.StatusSeeOther, recorder.Code, "unexpected response:", recorder) actualLocation := recorder.Header().Get("Location") r.True( strings.HasPrefix(actualLocation, expectedRedirectLocationPrefix), @@ -159,7 +170,7 @@ func TestManager(t *testing.T) { // Check just enough of the response to ensure that we wired up the callback endpoint correctly. // The endpoint's own unit tests cover everything else. - r.Equal(http.StatusSeeOther, recorder.Code) + r.Equal(http.StatusSeeOther, recorder.Code, "unexpected response:", recorder) actualLocation := recorder.Header().Get("Location") r.True( strings.HasPrefix(actualLocation, downstreamRedirectURL), @@ -171,7 +182,7 @@ func TestManager(t *testing.T) { actualLocationQueryParams := parsedLocation.Query() r.Contains(actualLocationQueryParams, "code") r.Equal("openid username groups", actualLocationQueryParams.Get("scope")) - r.Equal("some-state-value-with-enough-bytes-to-exceed-min-allowed", actualLocationQueryParams.Get("state")) + r.Equal("some-state", actualLocationQueryParams.Get("state")) // Make sure that we wired up the callback endpoint to use kube storage for fosite sessions. r.Equal(len(kubeClient.Actions()), numberOfKubeActionsBeforeThisRequest+3, @@ -198,8 +209,8 @@ func TestManager(t *testing.T) { r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right endpoint was called + r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) var body map[string]interface{} - r.Equal(http.StatusOK, recorder.Code) r.NoError(json.Unmarshal(recorder.Body.Bytes(), &body)) r.Contains(body, "id_token") r.Contains(body, "access_token") @@ -228,7 +239,7 @@ func TestManager(t *testing.T) { r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right JWKS endpoint was called - r.Equal(http.StatusOK, recorder.Code) + r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) parsedJWKSResult := jose.JSONWebKeySet{} @@ -246,31 +257,53 @@ func TestManager(t *testing.T) { } dynamicJWKSProvider = jwks.NewDynamicJWKSProvider() - parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) + parsedUpstreamIDPAuthorizationURL1, err := url.Parse(upstreamIDPAuthorizationURL1) + r.NoError(err) + parsedUpstreamIDPAuthorizationURL2, err := url.Parse(upstreamIDPAuthorizationURL2) r.NoError(err) federationDomainIDPs = []*federationdomainproviders.FederationDomainIdentityProvider{ { - DisplayName: upstreamIDPName, - UID: upstreamResourceUID, + DisplayName: upstreamIDPDisplayName1, + UID: upstreamResourceUID1, + Transforms: idtransform.NewTransformationPipeline(), + }, + { + DisplayName: upstreamIDPDisplayName2, + UID: upstreamResourceUID2, Transforms: idtransform.NewTransformationPipeline(), }, } - idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). - WithName(upstreamIDPName). - WithClientID("test-client-id"). - WithResourceUID(upstreamResourceUID). - WithAuthorizationURL(*parsedUpstreamIDPAuthorizationURL). - WithScopes([]string{"test-scope"}). - WithIDTokenClaim("iss", "https://some-issuer.com"). - WithIDTokenClaim("sub", "some-subject"). - WithIDTokenClaim("username", "test-username"). - WithIDTokenClaim("groups", "test-group1"). - WithRefreshToken("some-opaque-token"). - WithoutAccessToken(). - Build(), - ).BuildDynamicUpstreamIDPProvider() + idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName(upstreamIDPName1). + WithClientID("test-client-id-1"). + WithResourceUID(upstreamResourceUID1). + WithAuthorizationURL(*parsedUpstreamIDPAuthorizationURL1). + WithScopes([]string{"test-scope"}). + WithIDTokenClaim("iss", "https://some-issuer.com"). + WithIDTokenClaim("sub", "some-subject"). + WithIDTokenClaim("username", "test-username"). + WithIDTokenClaim("groups", "test-group1"). + WithRefreshToken("some-opaque-token"). + WithoutAccessToken(). + Build(), + ). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName(upstreamIDPName2). + WithClientID("test-client-id-2"). + WithResourceUID(upstreamResourceUID2). + WithAuthorizationURL(*parsedUpstreamIDPAuthorizationURL2). + WithScopes([]string{"test-scope"}). + WithIDTokenClaim("iss", "https://some-issuer.com"). + WithIDTokenClaim("sub", "some-subject"). + WithIDTokenClaim("username", "test-username"). + WithIDTokenClaim("groups", "test-group1"). + WithRefreshToken("some-opaque-token"). + WithoutAccessToken(). + Build(), + ).BuildDynamicUpstreamIDPProvider() kubeClient = fake.NewSimpleClientset() secretsClient := kubeClient.CoreV1().Secrets("some-namespace") @@ -326,14 +359,14 @@ func TestManager(t *testing.T) { requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2) requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2) - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1, "", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "?some=query", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1, "", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "?some=query", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) // Hostnames are case-insensitive, so test that we can handle that. - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) - requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", upstreamIDPName, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", []string{upstreamIDPDisplayName1, upstreamIDPDisplayName2}, upstreamIDPType, upstreamIDPFlows) issuer1JWKS := requireJWKSRequestToBeHandled(issuer1, "", issuer1KeyID) issuer2JWKS := requireJWKSRequestToBeHandled(issuer2, "", issuer2KeyID) @@ -344,35 +377,50 @@ func TestManager(t *testing.T) { requireJWKSRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2KeyID) requireJWKSRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2KeyID) - authRequestParams := "?" + url.Values{ - "pinniped_idp_name": []string{upstreamIDPName}, + authRequestParamsIDP1 := "?" + url.Values{ + "pinniped_idp_name": []string{upstreamIDPDisplayName1}, "response_type": []string{"code"}, "scope": []string{"openid profile email username groups"}, "client_id": []string{downstreamClientID}, - "state": []string{"some-state-value-with-enough-bytes-to-exceed-min-allowed"}, + "state": []string{"some-state"}, "nonce": []string{"some-nonce-value-with-enough-bytes-to-exceed-min-allowed"}, "code_challenge": []string{testutil.SHA256(downstreamPKCECodeVerifier)}, "code_challenge_method": []string{"S256"}, "redirect_uri": []string{downstreamRedirectURL}, }.Encode() - requireAuthorizationRequestToBeHandled(issuer1, authRequestParams, upstreamIDPAuthorizationURL) - requireAuthorizationRequestToBeHandled(issuer2, authRequestParams, upstreamIDPAuthorizationURL) + authRequestParamsIDP2 := "?" + url.Values{ + "pinniped_idp_name": []string{upstreamIDPDisplayName2}, + "response_type": []string{"code"}, + "scope": []string{"openid profile email username groups"}, + "client_id": []string{downstreamClientID}, + "state": []string{"some-state"}, + "nonce": []string{"some-nonce-value-with-enough-bytes-to-exceed-min-allowed"}, + "code_challenge": []string{testutil.SHA256(downstreamPKCECodeVerifier)}, + "code_challenge_method": []string{"S256"}, + "redirect_uri": []string{downstreamRedirectURL}, + }.Encode() + + requireAuthorizationRequestToBeHandled(issuer1, authRequestParamsIDP1, upstreamIDPAuthorizationURL1) + requireAuthorizationRequestToBeHandled(issuer2, authRequestParamsIDP1, upstreamIDPAuthorizationURL1) + requireAuthorizationRequestToBeHandled(issuer1, authRequestParamsIDP2, upstreamIDPAuthorizationURL2) + requireAuthorizationRequestToBeHandled(issuer2, authRequestParamsIDP2, upstreamIDPAuthorizationURL2) // Hostnames are case-insensitive, so test that we can handle that. csrfCookieValue1, upstreamStateParam1 := - requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL) + requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParamsIDP1, upstreamIDPAuthorizationURL1) csrfCookieValue2, upstreamStateParam2 := - requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL) + requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParamsIDP1, upstreamIDPAuthorizationURL1) - callbackRequestParams1 := "?" + url.Values{ - "code": []string{"some-fake-code"}, - "state": []string{upstreamStateParam1}, - }.Encode() - callbackRequestParams2 := "?" + url.Values{ - "code": []string{"some-fake-code"}, - "state": []string{upstreamStateParam2}, - }.Encode() + csrfCookieValue3, upstreamStateParam3 := + requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParamsIDP2, upstreamIDPAuthorizationURL2) + csrfCookieValue4, upstreamStateParam4 := + requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParamsIDP2, upstreamIDPAuthorizationURL2) + + callbackRequestParams1 := "?" + url.Values{"code": []string{"some-fake-code"}, "state": []string{upstreamStateParam1}}.Encode() + callbackRequestParams2 := "?" + url.Values{"code": []string{"some-fake-code"}, "state": []string{upstreamStateParam2}}.Encode() + callbackRequestParams3 := "?" + url.Values{"code": []string{"some-fake-code"}, "state": []string{upstreamStateParam3}}.Encode() + callbackRequestParams4 := "?" + url.Values{"code": []string{"some-fake-code"}, "state": []string{upstreamStateParam4}}.Encode() downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams1, csrfCookieValue1) downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams2, csrfCookieValue2) @@ -381,12 +429,17 @@ func TestManager(t *testing.T) { downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams1, csrfCookieValue1) downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams2, csrfCookieValue2) + downstreamAuthCode5 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams3, csrfCookieValue3) + downstreamAuthCode6 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams4, csrfCookieValue4) + requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1) requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2) // Hostnames are case-insensitive, so test that we can handle that. requireTokenRequestToBeHandled(issuer1DifferentCaseHostname, downstreamAuthCode3, issuer1JWKS, issuer1) requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode4, issuer2JWKS, issuer2) + requireTokenRequestToBeHandled(issuer1DifferentCaseHostname, downstreamAuthCode5, issuer1JWKS, issuer1) + requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode6, issuer2JWKS, issuer2) } when("given some valid providers via SetFederationDomains()", func() { From 61bb01b31df54c50b8c14756751db9886aa5a653 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 19 Jul 2023 13:58:56 -0700 Subject: [PATCH 50/81] extract a helper function in federation_domain_watcher.go Co-authored-by: Benjamin A. Petersen --- .../federation_domain_watcher.go | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index e40f9c447..6de4b784b 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -420,32 +420,11 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic }) } - if len(idpNotFoundIndices) != 0 { - msgs := []string{} - for _, idpNotFoundIndex := range idpNotFoundIndices { - msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, - federationDomain.Spec.IdentityProviders[idpNotFoundIndex].DisplayName)) - } - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProvidersObjectRefsNotFound, - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", - strings.Join(msgs, ", ")), - }) - } else if len(federationDomain.Spec.IdentityProviders) != 0 { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the resources specified by .spec.identityProviders[].objectRef were found", - }) - } - // This is the constructor for any case other than the legacy case, including when there is an empty list of IDPs. federationDomainIssuer, err := federationdomainproviders.NewFederationDomainIssuer(federationDomain.Spec.Issuer, federationDomainIdentityProviders) conditions = appendIssuerURLValidCondition(err, conditions) + conditions = appendIdentityProvidersFoundCondition(idpNotFoundIndices, federationDomain.Spec.IdentityProviders, conditions) conditions = appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames, conditions) conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), badKinds, conditions) @@ -456,6 +435,7 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic return federationDomainIssuer, conditions, nil } + func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef corev1.TypedLocalObjectReference, namespace string) (types.UID, bool, error) { var idpResourceUID types.UID var foundIDP metav1.Object @@ -696,6 +676,35 @@ func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuf return conditions } +func appendIdentityProvidersFoundCondition( + idpNotFoundIndices []int, + federationDomainIdentityProviders []configv1alpha1.FederationDomainIdentityProvider, + conditions []*configv1alpha1.Condition, +) []*configv1alpha1.Condition { + if len(idpNotFoundIndices) != 0 { + msgs := []string{} + for _, idpNotFoundIndex := range idpNotFoundIndices { + msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, + federationDomainIdentityProviders[idpNotFoundIndex].DisplayName)) + } + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", + strings.Join(msgs, ", ")), + }) + } else if len(federationDomainIdentityProviders) != 0 { + conditions = append(conditions, &configv1alpha1.Condition{ + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + }) + } + return conditions +} + func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if len(badSuffixNames) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ From 4b75ced52c135a6fa7a724bb06e41f9e4bde96e6 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 19 Jul 2023 14:26:50 -0700 Subject: [PATCH 51/81] add unit tests for getters in federation_domain_issuer_test.go --- .../federation_domain_issuer_test.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_issuer_test.go b/internal/federationdomain/federationdomainproviders/federation_domain_issuer_test.go index cd4c97fba..34125d746 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_issuer_test.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_issuer_test.go @@ -4,6 +4,7 @@ package federationdomainproviders import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -105,3 +106,34 @@ func TestFederationDomainIssuerValidations(t *testing.T) { }) } } + +func TestFederationDomainGetters(t *testing.T) { + const issuerHost = "some-issuer.com" + const issuerPath = "/some/path" + issuerURLString := fmt.Sprintf("https://%s%s", issuerHost, issuerPath) + + provider1 := &FederationDomainIdentityProvider{ + DisplayName: "test-name-1", + UID: "test-uid-1", + } + provider2 := &FederationDomainIdentityProvider{ + DisplayName: "test-name-2", + UID: "test-uid-2", + } + + fdi, err := NewFederationDomainIssuer(issuerURLString, []*FederationDomainIdentityProvider{provider1, provider2}) + require.NoError(t, err) + require.Equal(t, issuerURLString, fdi.Issuer()) + require.Equal(t, issuerHost, fdi.IssuerHost()) + require.Equal(t, issuerPath, fdi.IssuerPath()) + require.Equal(t, []*FederationDomainIdentityProvider{provider1, provider2}, fdi.IdentityProviders()) + require.Nil(t, fdi.DefaultIdentityProvider()) + + fdi, err = NewFederationDomainIssuerWithDefaultIDP(issuerURLString, provider1) + require.NoError(t, err) + require.Equal(t, issuerURLString, fdi.Issuer()) + require.Equal(t, issuerHost, fdi.IssuerHost()) + require.Equal(t, issuerPath, fdi.IssuerPath()) + require.Equal(t, []*FederationDomainIdentityProvider{provider1}, fdi.IdentityProviders()) + require.Equal(t, provider1, fdi.DefaultIdentityProvider()) +} From 84041e0c55c5b92b6380ba84247b48a8735a2774 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 19 Jul 2023 14:56:46 -0700 Subject: [PATCH 52/81] add unit test for ApplyIdentityTransformations helper --- internal/celtransformer/celformer.go | 2 +- internal/celtransformer/celformer_test.go | 2 +- .../federation_domain_watcher_test.go | 2 +- .../downstreamsession/downstream_session.go | 3 +- .../downstream_session_test.go | 86 +++++++++++++++++++ 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/internal/celtransformer/celformer.go b/internal/celtransformer/celformer.go index 085af4502..e88b5bfda 100644 --- a/internal/celtransformer/celformer.go +++ b/internal/celtransformer/celformer.go @@ -29,7 +29,7 @@ const ( constStringVariableName = "strConst" constStringListVariableName = "strListConst" - DefaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy" + DefaultPolicyRejectedAuthMessage = "authentication was rejected by a configured policy" ) // CELTransformer can compile any number of transformation expression pipelines. diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index a38a1b236..10a148b1e 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -101,7 +101,7 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"admins", "developers", "other"}, wantAuthRejected: true, - wantAuthRejectedMessage: `Authentication was rejected by a configured policy`, + wantAuthRejectedMessage: `authentication was rejected by a configured policy`, }, { name: "any transformations can use the username and group variables", diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 4bf20fafb..41287aeab 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -1953,7 +1953,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Username: "rejectMeWithDefaultMessage", Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ Rejected: true, - Message: "Authentication was rejected by a configured policy", // this is the default message + Message: "authentication was rejected by a configured policy", // this is the default message }, }, }, diff --git a/internal/federationdomain/downstreamsession/downstream_session.go b/internal/federationdomain/downstreamsession/downstream_session.go index 6a5f70fe8..c7f3b4e8e 100644 --- a/internal/federationdomain/downstreamsession/downstream_session.go +++ b/internal/federationdomain/downstreamsession/downstream_session.go @@ -41,7 +41,6 @@ const ( emailVerifiedClaimInvalidFormatErr = constable.Error("email_verified claim in upstream ID token has invalid format") emailVerifiedClaimFalseErr = constable.Error("email_verified claim in upstream ID token has false value") idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error") - idTransformPolicyErr = constable.Error("configured identity policy rejected this authentication") ) // MakeDownstreamSession creates a downstream OIDC session. @@ -262,7 +261,7 @@ func ApplyIdentityTransformations( } if !transformationResult.AuthenticationAllowed { plog.Debug("authentication rejected by configured policy", "inputUsername", username, "inputGroups", groups) - return "", nil, idTransformPolicyErr + return "", nil, fmt.Errorf("configured identity policy rejected this authentication: %s", transformationResult.RejectedAuthenticationMessage) } plog.Debug("identity transformation successfully applied during authentication", "originalUsername", username, diff --git a/internal/federationdomain/downstreamsession/downstream_session_test.go b/internal/federationdomain/downstreamsession/downstream_session_test.go index d6effd4b1..ea584934f 100644 --- a/internal/federationdomain/downstreamsession/downstream_session_test.go +++ b/internal/federationdomain/downstreamsession/downstream_session_test.go @@ -4,10 +4,14 @@ package downstreamsession import ( + "context" "testing" + "time" "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/celtransformer" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/testutil/oidctestutil" ) @@ -70,3 +74,85 @@ func TestMapAdditionalClaimsFromUpstreamIDToken(t *testing.T) { }) } } + +func TestApplyIdentityTransformations(t *testing.T) { + tests := []struct { + name string + transforms []celtransformer.CELTransformation + username string + groups []string + wantUsername string + wantGroups []string + wantErr string + }{ + { + name: "unexpected errors", + transforms: []celtransformer.CELTransformation{ + &celtransformer.UsernameTransformation{Expression: `""`}, + }, + username: "ryan", + groups: []string{"a", "b"}, + wantErr: "configured identity transformation or policy resulted in unexpected error", + }, + { + name: "auth disallowed by policy with implicit rejection message", + transforms: []celtransformer.CELTransformation{ + &celtransformer.AllowAuthenticationPolicy{Expression: `false`}, + }, + username: "ryan", + groups: []string{"a", "b"}, + wantErr: "configured identity policy rejected this authentication: authentication was rejected by a configured policy", + }, + { + name: "auth disallowed by policy with explicit rejection message", + transforms: []celtransformer.CELTransformation{ + &celtransformer.AllowAuthenticationPolicy{ + Expression: `false`, + RejectedAuthenticationMessage: "this is the stated reason", + }, + }, + username: "ryan", + groups: []string{"a", "b"}, + wantErr: "configured identity policy rejected this authentication: this is the stated reason", + }, + { + name: "successful auth", + transforms: []celtransformer.CELTransformation{ + &celtransformer.UsernameTransformation{Expression: `"pre:" + username`}, + &celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`}, + }, + username: "ryan", + groups: []string{"a", "b"}, + wantUsername: "pre:ryan", + wantGroups: []string{"pre:a", "pre:b"}, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + transformer, err := celtransformer.NewCELTransformer(5 * time.Second) + require.NoError(t, err) + + pipeline := idtransform.NewTransformationPipeline() + for _, transform := range tt.transforms { + compiledTransform, err := transformer.CompileTransformation(transform, nil) + require.NoError(t, err) + pipeline.AppendTransformation(compiledTransform) + } + + gotUsername, gotGroups, err := ApplyIdentityTransformations(context.Background(), pipeline, tt.username, tt.groups) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, gotUsername) + require.Nil(t, gotGroups) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantUsername, gotUsername) + require.Equal(t, tt.wantGroups, gotGroups) + } + }) + } +} From 23ed2856ce9c7ddf204c34c947d0f5babd3a6d78 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 19 Jul 2023 15:12:12 -0700 Subject: [PATCH 53/81] small refactor in supervisor_discovery_test.go --- test/integration/supervisor_discovery_test.go | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 4e8d061ca..3d8cc2d75 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/retry" + "k8s.io/utils/strings/slices" "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -132,38 +133,14 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving. config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuer6}, v1alpha1.FederationDomainPhaseError) - requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionFalse, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, - "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, - "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, - "TransformsExpressionsValid": v1alpha1.ConditionTrue, - "TransformsExamplesPassed": v1alpha1.ConditionTrue, - }) - requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionFalse, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, - "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, - "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, - "TransformsExpressionsValid": v1alpha1.ConditionTrue, - "TransformsExamplesPassed": v1alpha1.ConditionTrue, - }) + requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"})) + requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"})) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) // If we delete the first duplicate issuer, the second duplicate issuer starts serving. requireDelete(t, client, ns, config6Duplicate1.Name) requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6, nil) - requireFullySuccessfulStatus(t, client, ns, config6Duplicate2.Name) + requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) // When we finally delete all issuers, the endpoint should be down. requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config6Duplicate2, client, ns, scheme, addr, caBundle, issuer6) @@ -175,19 +152,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When we create a provider with an invalid issuer, the status is set to invalid. badConfig := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: badIssuer}, v1alpha1.FederationDomainPhaseError) - requireStatus(t, client, ns, badConfig.Name, v1alpha1.FederationDomainPhaseError, map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionFalse, - "IssuerIsUnique": v1alpha1.ConditionTrue, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionFalse, - "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, - "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, - "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, - "TransformsExpressionsValid": v1alpha1.ConditionTrue, - "TransformsExamplesPassed": v1alpha1.ConditionTrue, - }) + requireStatus(t, client, ns, badConfig.Name, v1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerURLValid"})) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) }) @@ -225,7 +190,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { Issuer: issuer1, TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName1}, }, v1alpha1.FederationDomainPhaseReady) - requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) + requireStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) // The spec.tls.secretName Secret does not exist, so the endpoints should fail with TLS errors. requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady(t, issuer1) @@ -269,7 +234,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { Issuer: issuer2, TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName2}, }, v1alpha1.FederationDomainPhaseReady) - requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) + requireStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) // Create the Secret. ca2 := createTLSCertificateSecret(ctx, t, ns, hostname2, nil, certSecretName2, kubeClient) @@ -319,7 +284,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { // Create an FederationDomain without a spec.tls.secretName. federationDomain1 := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuerUsingIPAddress}, v1alpha1.FederationDomainPhaseReady) - requireFullySuccessfulStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name) + requireStatus(t, pinnipedClient, federationDomain1.Namespace, federationDomain1.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) // There is no default TLS cert and the spec.tls.secretName was not set, so the endpoints should fail with TLS errors. requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady(t, issuerUsingIPAddress) @@ -337,7 +302,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { Issuer: issuerUsingHostname, TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: certSecretName}, }, v1alpha1.FederationDomainPhaseReady) - requireFullySuccessfulStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name) + requireStatus(t, pinnipedClient, federationDomain2.Namespace, federationDomain2.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) // Create the Secret. certCA := createTLSCertificateSecret(ctx, t, ns, hostname, nil, certSecretName, kubeClient) @@ -525,7 +490,7 @@ func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear( t.Helper() newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{Issuer: issuerName}, v1alpha1.FederationDomainPhaseReady) jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) - requireFullySuccessfulStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name) + requireStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name, v1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) return newFederationDomain, jwksResult } @@ -693,8 +658,8 @@ func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name st require.NoError(t, err) } -func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string) { - requireStatus(t, client, ns, name, v1alpha1.FederationDomainPhaseReady, map[string]v1alpha1.ConditionStatus{ +func withAllSuccessfulConditions() map[string]v1alpha1.ConditionStatus { + return map[string]v1alpha1.ConditionStatus{ "Ready": v1alpha1.ConditionTrue, "IssuerIsUnique": v1alpha1.ConditionTrue, "IdentityProvidersFound": v1alpha1.ConditionTrue, @@ -706,7 +671,19 @@ func requireFullySuccessfulStatus(t *testing.T, client pinnipedclientset.Interfa "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, "TransformsExamplesPassed": v1alpha1.ConditionTrue, - }) + } +} + +func withFalseConditions(falseConditionTypes []string) map[string]v1alpha1.ConditionStatus { + c := map[string]v1alpha1.ConditionStatus{} + for k, v := range withAllSuccessfulConditions() { + if slices.Contains(falseConditionTypes, k) { + c[k] = v1alpha1.ConditionFalse + } else { + c[k] = v + } + } + return c } func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, wantPhase v1alpha1.FederationDomainPhase, wantConditionTypeToStatus map[string]v1alpha1.ConditionStatus) { From 53413220716ef72842bd9f52eac2f78cce44e03d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 20 Jul 2023 15:49:01 -0700 Subject: [PATCH 54/81] add integration test for FederationDomain status updates - Also fix small bug in controller where it used Sprintf wrong - Rename WaitForTestFederationDomainStatus test helper to WaitForFederationDomainStatusPhase --- .../federation_domain_watcher.go | 8 +- .../federation_domain_watcher_test.go | 2 +- test/integration/e2e_test.go | 28 +- ...supervisor_federationdomain_status_test.go | 268 ++++++++++++++++++ test/integration/supervisor_login_test.go | 2 +- test/integration/supervisor_warnings_test.go | 6 +- test/testlib/client.go | 71 ++++- 7 files changed, 349 insertions(+), 36 deletions(-) create mode 100644 test/integration/supervisor_federationdomain_status_test.go diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 6de4b784b..98f91fb72 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -35,6 +35,8 @@ import ( ) const ( + controllerName = "FederationDomainWatcherController" + typeReady = "Ready" typeIssuerURLValid = "IssuerURLValid" typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" @@ -109,7 +111,7 @@ func NewFederationDomainWatcherController( allowedKinds := sets.New(kindActiveDirectoryIdentityProvider, kindLDAPIdentityProvider, kindOIDCIdentityProvider) return controllerlib.New( controllerlib.Config{ - Name: "FederationDomainWatcherController", + Name: controllerName, Syncer: &federationDomainWatcherController{ federationDomainsSetter: federationDomainsSetter, apiGroup: fmt.Sprintf("idp.supervisor.%s", apiGroupSuffix), @@ -305,7 +307,7 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( Status: configv1alpha1.ConditionFalse, Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ - "and %q identity provider resources have been found: "+ + "and %d identity provider resources have been found: "+ "please update .spec.identityProviders to specify which identity providers "+ "this federation domain should use", idpCRsCount), }) @@ -850,7 +852,7 @@ func (c *federationDomainWatcherController) updateStatus( } _ = conditionsutil.MergeConfigConditions(conditions, - federationDomain.Generation, &updated.Status.Conditions, plog.New(), metav1.NewTime(c.clock.Now())) + federationDomain.Generation, &updated.Status.Conditions, plog.New().WithName(controllerName), metav1.NewTime(c.clock.Now())) if equality.Semantic.DeepEqual(federationDomain, updated) { return nil diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 41287aeab..504e3596e 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -361,7 +361,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { LastTransitionTime: time, Reason: "IdentityProviderNotSpecified", Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ - "and %q identity provider resources have been found: "+ + "and %d identity provider resources have been found: "+ "please update .spec.identityProviders to specify which identity providers "+ "this federation domain should use", idpCRsCount), } diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 09bf7e315..dd3741784 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -158,7 +158,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -241,7 +241,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -326,7 +326,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -447,7 +447,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -575,7 +575,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -645,7 +645,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -718,7 +718,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -774,7 +774,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -834,7 +834,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -902,7 +902,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -958,7 +958,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1028,7 +1028,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1080,7 +1080,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1132,7 +1132,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go new file mode 100644 index 000000000..762b95048 --- /dev/null +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -0,0 +1,268 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + + "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/test/testlib" +) + +// Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go. +func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { + env := testlib.IntegrationEnv(t) + client := testlib.NewSupervisorClientset(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret(ctx, t, env.SupervisorNamespace, defaultTLSCertSecretName(env), client, testlib.NewKubernetesClientset(t)) + + tests := []struct { + name string + run func(t *testing.T) + }{ + { + name: "valid spec in without explicit identity providers makes status error unless there is exactly one identity provider", + run: func(t *testing.T) { + // Creating FederationDomain without any explicit IDPs should put the FederationDomain into an error status. + fd := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com/fake", + }, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulLegacyFederationDomainConditions("", fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "LegacyConfigurationIdentityProviderNotFound", + Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource", + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + }, + )) + + // Creating an IDP should put the FederationDomain into a successful status. + oidcIdentityProvider1 := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, + allSuccessfulLegacyFederationDomainConditions(oidcIdentityProvider1.Name, fd.Spec)) + + // Creating a second IDP should put the FederationDomain back into an error status again. + oidcIdentityProvider2 := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulLegacyFederationDomainConditions(oidcIdentityProvider2.Name, fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProviderNotSpecified", + Message: "no resources were specified by .spec.identityProviders[].objectRef and 2 identity provider " + + "resources have been found: please update .spec.identityProviders to specify which identity providers " + + "this federation domain should use", + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + }, + )) + }, + }, + { + name: "valid spec with explicit identity providers makes status error until those identity providers all exist", + run: func(t *testing.T) { + oidcIDP1Meta := testlib.ObjectMetaWithRandomName(t, "upstream-oidc-idp") + oidcIDP2Meta := testlib.ObjectMetaWithRandomName(t, "upstream-oidc-idp") + // Creating FederationDomain with explicit IDPs that don't exist should put the FederationDomain into an error status. + fd := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com/fake", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "idp1", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIDP1Meta.Name, + }, + Transforms: v1alpha1.FederationDomainTransforms{}, + }, + { + DisplayName: "idp2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIDP2Meta.Name, + }, + Transforms: v1alpha1.FederationDomainTransforms{}, + }, + }, + }, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulFederationDomainConditions(fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", + Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[0] with displayName "idp1", .spec.identityProviders[1] with displayName "idp2"`, + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + }, + )) + + // Creating the first IDP should not be enough to put the FederationDomain into a successful status. + oidcIdentityProvider1 := testlib.CreateTestOIDCIdentityProviderWithObjectMeta(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, oidcIDP1Meta, idpv1alpha1.PhaseError) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulFederationDomainConditions(fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", + Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[1] with displayName "idp2"`, + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + }, + )) + + // Creating the second IDP should put the FederationDomain into a successful status. + testlib.CreateTestOIDCIdentityProviderWithObjectMeta(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, oidcIDP2Meta, idpv1alpha1.PhaseError) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, + allSuccessfulFederationDomainConditions(fd.Spec)) + + // Removing one IDP should put the FederationDomain back into an error status again. + oidcIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().OIDCIdentityProviders(env.SupervisorNamespace) + err := oidcIDPClient.Delete(ctx, oidcIdentityProvider1.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulFederationDomainConditions(fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", + Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[0] with displayName "idp1"`, + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + }, + )) + }, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + tt.run(t) + }) + } +} + +func replaceSomeConditions(conditions []v1alpha1.Condition, replaceWithTheseConditions []v1alpha1.Condition) []v1alpha1.Condition { + cp := make([]v1alpha1.Condition, len(conditions)) + copy(cp, conditions) + for _, replacementCond := range replaceWithTheseConditions { + for i, cond := range cp { + if replacementCond.Type == cond.Type { + cp[i] = replacementCond + break + } + } + } + return cp +} + +func allSuccessfulLegacyFederationDomainConditions(idpName string, federationDomainSpec v1alpha1.FederationDomainSpec) []v1alpha1.Condition { + return replaceSomeConditions( + allSuccessfulFederationDomainConditions(federationDomainSpec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "True", Reason: "LegacyConfigurationSuccess", + Message: fmt.Sprintf(`no resources were specified by .spec.identityProviders[].objectRef but exactly one `+ + `identity provider resource has been found: using "%s" as identity provider: `+ + `please explicitly list identity providers in .spec.identityProviders `+ + `(this legacy configuration mode may be removed in a future version of Pinniped)`, idpName), + }, + }, + ) +} + +func allSuccessfulFederationDomainConditions(federationDomainSpec v1alpha1.FederationDomainSpec) []v1alpha1.Condition { + return []v1alpha1.Condition{ + { + Type: "IdentityProvidersDisplayNamesUnique", Status: "True", Reason: "Success", + Message: "the names specified by .spec.identityProviders[].displayName are unique", + }, + { + Type: "IdentityProvidersFound", Status: "True", Reason: "Success", + Message: "the resources specified by .spec.identityProviders[].objectRef were found", + }, + { + Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "True", Reason: "Success", + Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", + }, + { + Type: "IdentityProvidersObjectRefKindValid", Status: "True", Reason: "Success", + Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", + }, + { + Type: "IssuerIsUnique", Status: "True", Reason: "Success", + Message: "spec.issuer is unique among all FederationDomains", + }, + { + Type: "IssuerURLValid", Status: "True", Reason: "Success", + Message: "spec.issuer is a valid URL", + }, + { + Type: "OneTLSSecretPerIssuerHostname", Status: "True", Reason: "Success", + Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", + }, + { + Type: "Ready", Status: "True", Reason: "Success", + Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ + "the discovery endpoint is %s/.well-known/openid-configuration", federationDomainSpec.Issuer), + }, + { + Type: "TransformsConstantsNamesUnique", Status: "True", Reason: "Success", + Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", + }, + { + Type: "TransformsExamplesPassed", Status: "True", Reason: "Success", + Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", + }, + { + Type: "TransformsExpressionsValid", Status: "True", Reason: "Success", + Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", + }, + } +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 3fe6c70db..dcaf45d9f 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -2091,7 +2091,7 @@ func testSupervisorLogin( idpName := createIDP(t) // Now that both the FederationDomain and the IDP are created, the FederationDomain should be ready. - testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for // the `/jwks.json` endpoint to succeed, because there is no point in proceeding and eventually diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index 9f6df59f2..3327bd4ad 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -109,7 +109,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" @@ -254,7 +254,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { sAMAccountName := expectedUsername + "@" + env.SupervisorUpstreamActiveDirectory.Domain setupClusterForEndToEndActiveDirectoryTest(t, sAMAccountName, env) - testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" @@ -394,7 +394,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForTestFederationDomainStatus(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml" diff --git a/test/testlib/client.go b/test/testlib/client.go index 9fba764b2..c80c2e20d 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -304,31 +304,57 @@ func CreateTestFederationDomain( }) // Wait for the FederationDomain to enter the expected phase (or time out). - WaitForTestFederationDomainStatus(ctx, t, federationDomain.Name, expectStatus) + WaitForFederationDomainStatusPhase(ctx, t, federationDomain.Name, expectStatus) return federationDomain } -func WaitForTestFederationDomainStatus(ctx context.Context, t *testing.T, federationDomainName string, expectStatus configv1alpha1.FederationDomainPhase) { +func WaitForFederationDomainStatusPhase(ctx context.Context, t *testing.T, federationDomainName string, expectPhase configv1alpha1.FederationDomainPhase) { t.Helper() testEnv := IntegrationEnv(t) federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) - var result *configv1alpha1.FederationDomain RequireEventuallyf(t, func(requireEventually *require.Assertions) { - var err error - result, err = federationDomainsClient.Get(ctx, federationDomainName, metav1.GetOptions{}) + fd, err := federationDomainsClient.Get(ctx, federationDomainName, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventually.Equal(expectStatus, result.Status.Phase) + requireEventually.Equalf(expectPhase, fd.Status.Phase, "actual status conditions were: %#v", fd.Status.Conditions) // If the FederationDomain was successfully created, ensure all secrets are present before continuing - if expectStatus == configv1alpha1.FederationDomainPhaseReady { - requireEventually.NotEmpty(result.Status.Secrets.JWKS.Name, "expected status.secrets.jwks.name not to be empty") - requireEventually.NotEmpty(result.Status.Secrets.TokenSigningKey.Name, "expected status.secrets.tokenSigningKey.name not to be empty") - requireEventually.NotEmpty(result.Status.Secrets.StateSigningKey.Name, "expected status.secrets.stateSigningKey.name not to be empty") - requireEventually.NotEmpty(result.Status.Secrets.StateEncryptionKey.Name, "expected status.secrets.stateEncryptionKey.name not to be empty") + if expectPhase == configv1alpha1.FederationDomainPhaseReady { + requireEventually.NotEmpty(fd.Status.Secrets.JWKS.Name, "expected status.secrets.jwks.name not to be empty") + requireEventually.NotEmpty(fd.Status.Secrets.TokenSigningKey.Name, "expected status.secrets.tokenSigningKey.name not to be empty") + requireEventually.NotEmpty(fd.Status.Secrets.StateSigningKey.Name, "expected status.secrets.stateSigningKey.name not to be empty") + requireEventually.NotEmpty(fd.Status.Secrets.StateEncryptionKey.Name, "expected status.secrets.stateEncryptionKey.name not to be empty") + } + }, 60*time.Second, 1*time.Second, "expected the FederationDomain to have status %q", expectPhase) +} + +func WaitForFederationDomainStatusConditions(ctx context.Context, t *testing.T, federationDomainName string, expectConditions []configv1alpha1.Condition) { + t.Helper() + testEnv := IntegrationEnv(t) + federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) + + RequireEventuallyf(t, func(requireEventually *require.Assertions) { + fd, err := federationDomainsClient.Get(ctx, federationDomainName, metav1.GetOptions{}) + requireEventually.NoError(err) + + requireEventually.Lenf(fd.Status.Conditions, len(expectConditions), + "wanted status conditions: %#v", expectConditions) + + for i, wantCond := range expectConditions { + actualCond := fd.Status.Conditions[i] + + // This is a cheat to avoid needing to make equality assertions on these fields. + requireEventually.NotZero(actualCond.LastTransitionTime) + wantCond.LastTransitionTime = actualCond.LastTransitionTime + requireEventually.NotZero(actualCond.ObservedGeneration) + wantCond.ObservedGeneration = actualCond.ObservedGeneration + + requireEventually.Equalf(wantCond, actualCond, + "wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d", + expectConditions, fd.Status.Conditions, i) } - }, 60*time.Second, 1*time.Second, "expected the FederationDomain to have status %q", expectStatus) + }, 60*time.Second, 1*time.Second, "wanted FederationDomain conditions") } func RandBytes(t *testing.T, numBytes int) []byte { @@ -475,6 +501,11 @@ func createOIDCClientSecret(t *testing.T, forOIDCClient *configv1alpha1.OIDCClie } func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider { + t.Helper() + return CreateTestOIDCIdentityProviderWithObjectMeta(t, spec, testObjectMeta(t, "upstream-oidc-idp"), expectedPhase) +} + +func CreateTestOIDCIdentityProviderWithObjectMeta(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, objectMeta metav1.ObjectMeta, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider { t.Helper() env := IntegrationEnv(t) client := NewSupervisorClientset(t) @@ -485,7 +516,7 @@ func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityP // Create the OIDCIdentityProvider using GenerateName to get a random name. created, err := upstreams.Create(ctx, &idpv1alpha1.OIDCIdentityProvider{ - ObjectMeta: testObjectMeta(t, "upstream-oidc-idp"), + ObjectMeta: objectMeta, Spec: spec, }, metav1.CreateOptions{}) require.NoError(t, err) @@ -494,7 +525,11 @@ func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityP t.Cleanup(func() { t.Logf("cleaning up test OIDCIdentityProvider %s/%s", created.Namespace, created.Name) err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{}) - require.NoError(t, err) + notFound := k8serrors.IsNotFound(err) + // It's okay if it is not found, because it might have been deleted by another part of this test. + if !notFound { + require.NoErrorf(t, err, "could not cleanup test OIDCIdentityProvider %s/%s", created.Namespace, created.Name) + } }) t.Logf("created test OIDCIdentityProvider %s", created.Name) @@ -724,3 +759,11 @@ func testObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta { Annotations: map[string]string{"pinniped.dev/testName": t.Name()}, } } + +func ObjectMetaWithRandomName(t *testing.T, baseName string) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: fmt.Sprintf("test-%s-%s", baseName, RandHex(t, 8)), + Labels: map[string]string{"pinniped.dev/test": ""}, + Annotations: map[string]string{"pinniped.dev/testName": t.Name()}, + } +} From 51742366feeddb321947c31fc9ec5c3fec404e28 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 20 Jul 2023 16:22:29 -0700 Subject: [PATCH 55/81] wordsmith some FederationDomain status messages --- .../federation_domain_watcher.go | 37 +++++++------- .../federation_domain_watcher_test.go | 51 +++++++++++-------- ...supervisor_federationdomain_status_test.go | 11 ++-- test/testlib/client.go | 2 +- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 98f91fb72..6c467e923 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -664,7 +664,7 @@ func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuf Type: typeIdentityProvidersObjectRefKindValid, Status: configv1alpha1.ConditionFalse, Reason: reasonKindUnrecognized, - Message: fmt.Sprintf("the kinds specified by .spec.identityProviders[].objectRef.kind are not recognized (should be one of %s): %s", + Message: fmt.Sprintf("some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized (should be one of %s): %s", strings.Join(expectedKinds, ", "), strings.Join(sortAndQuote(badSuffixNames), ", ")), }) } else { @@ -684,17 +684,16 @@ func appendIdentityProvidersFoundCondition( conditions []*configv1alpha1.Condition, ) []*configv1alpha1.Condition { if len(idpNotFoundIndices) != 0 { - msgs := []string{} + messages := []string{} for _, idpNotFoundIndex := range idpNotFoundIndices { - msgs = append(msgs, fmt.Sprintf(".spec.identityProviders[%d] with displayName %q", idpNotFoundIndex, - federationDomainIdentityProviders[idpNotFoundIndex].DisplayName)) + messages = append(messages, fmt.Sprintf("cannot find resource specified by .spec.identityProviders[%d].objectRef (with name %q)", + idpNotFoundIndex, federationDomainIdentityProviders[idpNotFoundIndex].ObjectRef.Name)) } conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, - Reason: reasonIdentityProvidersObjectRefsNotFound, - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", - strings.Join(msgs, ", ")), + Type: typeIdentityProvidersFound, + Status: configv1alpha1.ConditionFalse, + Reason: reasonIdentityProvidersObjectRefsNotFound, + Message: strings.Join(messages, "\n\n"), }) } else if len(federationDomainIdentityProviders) != 0 { conditions = append(conditions, &configv1alpha1.Condition{ @@ -713,7 +712,7 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s Type: typeIdentityProvidersAPIGroupSuffixValid, Status: configv1alpha1.ConditionFalse, Reason: reasonAPIGroupNameUnrecognized, - Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be %q): %s", + Message: fmt.Sprintf("some API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be %q): %s", expectedSuffixName, strings.Join(sortAndQuote(badSuffixNames), ", ")), }) } else { @@ -727,13 +726,13 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s return conditions } -func appendTransformsExpressionsValidCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if len(errors) > 0 { +func appendTransformsExpressionsValidCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(messages) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeTransformsExpressionsValid, Status: configv1alpha1.ConditionFalse, Reason: reasonInvalidTransformsExpressions, - Message: strings.Join(errors, "\n\n"), + Message: strings.Join(messages, "\n\n"), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ @@ -746,13 +745,13 @@ func appendTransformsExpressionsValidCondition(errors []string, conditions []*co return conditions } -func appendTransformsExamplesPassedCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if len(errors) > 0 { +func appendTransformsExamplesPassedCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(messages) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeTransformsExamplesPassed, Status: configv1alpha1.ConditionFalse, Reason: reasonTransformsExamplesFailed, - Message: strings.Join(errors, "\n\n"), + Message: strings.Join(messages, "\n\n"), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ @@ -765,13 +764,13 @@ func appendTransformsExamplesPassedCondition(errors []string, conditions []*conf return conditions } -func appendTransformsConstantsNamesUniqueCondition(errors []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if len(errors) > 0 { +func appendTransformsConstantsNamesUniqueCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { + if len(messages) > 0 { conditions = append(conditions, &configv1alpha1.Condition{ Type: typeTransformsConstantsNamesUnique, Status: configv1alpha1.ConditionFalse, Reason: reasonDuplicateConstantsNames, - Message: strings.Join(errors, "\n\n"), + Message: strings.Join(messages, "\n\n"), }) } else { conditions = append(conditions, &configv1alpha1.Condition{ diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 504e3596e..5790ad9e1 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -367,14 +367,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(idpsNotFound string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "IdentityProvidersObjectRefsNotFound", - Message: fmt.Sprintf(".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: %s", idpsNotFound), + Message: errorMessages, } } @@ -484,7 +484,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "APIGroupUnrecognized", - Message: fmt.Sprintf("the API groups specified by .spec.identityProviders[].objectRef.apiGroup "+ + Message: fmt.Sprintf("some API groups specified by .spec.identityProviders[].objectRef.apiGroup "+ "are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badApiGroups), } } @@ -507,7 +507,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "KindUnrecognized", - Message: fmt.Sprintf(`the kinds specified by .spec.identityProviders[].objectRef.kind are `+ + Message: fmt.Sprintf(`some kinds specified by .spec.identityProviders[].objectRef.kind are `+ `not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds), } } @@ -1051,11 +1051,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[0] with displayName "cant-find-me", `+ - `.spec.identityProviders[1] with displayName "cant-find-me-either", `+ - `.spec.identityProviders[2] with displayName "cant-find-me-still"`, - frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( + `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "cant-find-me-name") + + cannot find resource specified by .spec.identityProviders[1].objectRef (with name "cant-find-me-either-name") + + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "cant-find-me-still-name")`, + ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1267,11 +1269,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[0] with displayName "name1", `+ - `.spec.identityProviders[1] with displayName "name2", `+ - `.spec.identityProviders[2] with displayName "name3"`, - frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( + `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "some-oidc-idp") + + cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp") + + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`, + ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1327,10 +1331,11 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[1] with displayName "name2", `+ - `.spec.identityProviders[2] with displayName "name3"`, - frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( + `cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp") + + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`, + ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -1823,9 +1828,13 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), frozenMetav1Now, 123), sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123), sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123), - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound( - `.spec.identityProviders[0] with displayName "not unique", .spec.identityProviders[1] with displayName "not unique", .spec.identityProviders[2] with displayName "name1"`, - frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( + `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "this will not be found") + + cannot find resource specified by .spec.identityProviders[1].objectRef (with name "foo") + + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "foo")`, + ), frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadKindCondition(`"this is wrong"`, frozenMetav1Now, 123), sadTransformationExpressionsCondition(here.Doc( diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index 762b95048..e630d4823 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -6,6 +6,7 @@ package integration import ( "context" "fmt" + "go.pinniped.dev/internal/here" "testing" "time" @@ -120,7 +121,11 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { []v1alpha1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", - Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[0] with displayName "idp1", .spec.identityProviders[1] with displayName "idp2"`, + Message: here.Docf(` + cannot find resource specified by .spec.identityProviders[0].objectRef (with name "%s") + + cannot find resource specified by .spec.identityProviders[1].objectRef (with name "%s")`, + oidcIDP1Meta.Name, oidcIDP2Meta.Name), }, { Type: "Ready", Status: "False", Reason: "NotReady", @@ -140,7 +145,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { []v1alpha1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", - Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[1] with displayName "idp2"`, + Message: fmt.Sprintf(`cannot find resource specified by .spec.identityProviders[1].objectRef (with name "%s")`, oidcIDP2Meta.Name), }, { Type: "Ready", Status: "False", Reason: "NotReady", @@ -168,7 +173,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { []v1alpha1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", - Message: `.spec.identityProviders[].objectRef identifies resource(s) that cannot be found: .spec.identityProviders[0] with displayName "idp1"`, + Message: fmt.Sprintf(`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "%s")`, oidcIDP1Meta.Name), }, { Type: "Ready", Status: "False", Reason: "NotReady", diff --git a/test/testlib/client.go b/test/testlib/client.go index c80c2e20d..6053a671d 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -354,7 +354,7 @@ func WaitForFederationDomainStatusConditions(ctx context.Context, t *testing.T, "wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d", expectConditions, fd.Status.Conditions, i) } - }, 60*time.Second, 1*time.Second, "wanted FederationDomain conditions") + }, 5*time.Second, 1*time.Second, "wanted FederationDomain conditions") } func RandBytes(t *testing.T, numBytes int) []byte { From bd5cabf0ff80df9ddd5fee6d43707d769290faca Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 20 Jul 2023 16:34:11 -0700 Subject: [PATCH 56/81] fix some here.Doc string indents in federation_domain_watcher_test.go To make things visually line up better. --- .../federation_domain_watcher_test.go | 158 +++++++++--------- ...supervisor_federationdomain_status_test.go | 2 +- 2 files changed, 80 insertions(+), 80 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 5790ad9e1..cfea69451 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -1431,19 +1431,19 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { []configv1alpha1.Condition{ sadTransformationExpressionsCondition(here.Doc( `spec.identityProvider[0].transforms.expressions[0].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting - | this is not a valid cel expression - | .....^ - - spec.identityProvider[0].transforms.expressions[1].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting - | this is also not a valid cel expression - | .....^ - - spec.identityProvider[0].transforms.expressions[3].expression was invalid: - CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting - | still not a valid cel expression - | ......^`, + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is also not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting + | still not a valid cel expression + | ......^`, ), frozenMetav1Now, 123), sadTransformationExamplesCondition( "unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid", @@ -1577,40 +1577,40 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { []configv1alpha1.Condition{ sadTransformationExamplesCondition(here.Doc( `.spec.identityProviders[0].transforms.examples[2] example failed: - expected: authentication to be rejected - actual: authentication was not rejected + expected: authentication to be rejected + actual: authentication was not rejected - .spec.identityProviders[0].transforms.examples[3] example failed: - expected: authentication not to be rejected - actual: authentication was rejected with message "only ryan allowed" + .spec.identityProviders[0].transforms.examples[3] example failed: + expected: authentication not to be rejected + actual: authentication was rejected with message "only ryan allowed" - .spec.identityProviders[0].transforms.examples[4] example failed: - expected: authentication rejection message "wrong message" - actual: authentication rejection message "only ryan allowed" + .spec.identityProviders[0].transforms.examples[4] example failed: + expected: authentication rejection message "wrong message" + actual: authentication rejection message "only ryan allowed" - .spec.identityProviders[0].transforms.examples[6] example failed: - expected: username "wrong" - actual: username "pre:ryan" + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: username "wrong" + actual: username "pre:ryan" - .spec.identityProviders[0].transforms.examples[6] example failed: - expected: groups [] - actual: groups ["pre:a", "pre:b"] + .spec.identityProviders[0].transforms.examples[6] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"] - .spec.identityProviders[0].transforms.examples[7] example failed: - expected: username "wrong" - actual: username "pre:ryan" + .spec.identityProviders[0].transforms.examples[7] example failed: + expected: username "wrong" + actual: username "pre:ryan" - .spec.identityProviders[0].transforms.examples[8] example failed: - expected: groups ["wrong1", "wrong2"] - actual: groups ["pre:a", "pre:b"] + .spec.identityProviders[0].transforms.examples[8] example failed: + expected: groups ["wrong1", "wrong2"] + actual: groups ["pre:a", "pre:b"] - .spec.identityProviders[0].transforms.examples[9] example failed: - expected: username "" - actual: username "pre:ryan" + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: username "" + actual: username "pre:ryan" - .spec.identityProviders[0].transforms.examples[9] example failed: - expected: groups [] - actual: groups ["pre:a", "pre:b"]`, + .spec.identityProviders[0].transforms.examples[9] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"]`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1676,12 +1676,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { []configv1alpha1.Condition{ sadTransformationExamplesCondition(here.Doc( `.spec.identityProviders[0].transforms.examples[0] example failed: - expected: no transformation errors - actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed" - .spec.identityProviders[0].transforms.examples[1] example failed: - expected: no transformation errors - actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: no transformation errors + actual: transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1824,7 +1824,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadConstNamesUniqueCondition(here.Doc( `the names specified by .spec.identityProviders[0].transforms.constants[].name contain duplicates: "foo" - the names specified by .spec.identityProviders[1].transforms.constants[].name contain duplicates: "foo"`, + the names specified by .spec.identityProviders[1].transforms.constants[].name contain duplicates: "foo"`, ), frozenMetav1Now, 123), sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123), sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123), @@ -1839,41 +1839,41 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadKindCondition(`"this is wrong"`, frozenMetav1Now, 123), sadTransformationExpressionsCondition(here.Doc( `spec.identityProvider[2].transforms.expressions[1].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'does' expecting - | this does not compile - | .....^ - - spec.identityProvider[2].transforms.expressions[3].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'also' expecting - | this also does not compile - | .....^`, + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'does' expecting + | this does not compile + | .....^ + + spec.identityProvider[2].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'also' expecting + | this also does not compile + | .....^`, ), frozenMetav1Now, 123), sadTransformationExamplesCondition(here.Doc( `.spec.identityProviders[0].transforms.examples[0] example failed: - expected: username "this is wrong string" - actual: username "ryan:suffix" + expected: username "this is wrong string" + actual: username "ryan:suffix" - .spec.identityProviders[0].transforms.examples[0] example failed: - expected: groups ["this is wrong string list"] - actual: groups ["a", "b"] + .spec.identityProviders[0].transforms.examples[0] example failed: + expected: groups ["this is wrong string list"] + actual: groups ["a", "b"] - .spec.identityProviders[0].transforms.examples[1] example failed: - expected: username "this is also wrong string" - actual: username "ryan:suffix" + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: username "this is also wrong string" + actual: username "ryan:suffix" - .spec.identityProviders[0].transforms.examples[1] example failed: - expected: groups ["this is also wrong string list"] - actual: groups ["a", "b"] + .spec.identityProviders[0].transforms.examples[1] example failed: + expected: groups ["this is also wrong string list"] + actual: groups ["a", "b"] - .spec.identityProviders[1].transforms.examples[1] example failed: - expected: username "this is still wrong string" - actual: username "ryan:suffix" + .spec.identityProviders[1].transforms.examples[1] example failed: + expected: username "this is still wrong string" + actual: username "ryan:suffix" - .spec.identityProviders[1].transforms.examples[1] example failed: - expected: groups ["this is still wrong string list"] - actual: groups ["a", "b"] + .spec.identityProviders[1].transforms.examples[1] example failed: + expected: groups ["this is still wrong string list"] + actual: groups ["a", "b"] - unable to check if the examples specified by .spec.identityProviders[2].transforms.examples[] had errors because an expression was invalid`, + unable to check if the examples specified by .spec.identityProviders[2].transforms.examples[] had errors because an expression was invalid`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1889,14 +1889,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadTransformationExpressionsCondition(here.Doc( `spec.identityProvider[0].transforms.expressions[1].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'still' expecting - | this still does not compile - | .....^ - - spec.identityProvider[0].transforms.expressions[3].expression was invalid: - CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'really' expecting - | this really does not compile - | .....^`, + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'still' expecting + | this still does not compile + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'really' expecting + | this really does not compile + | .....^`, ), frozenMetav1Now, 123), sadTransformationExamplesCondition( "unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid", diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index e630d4823..d426d814a 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -6,7 +6,6 @@ package integration import ( "context" "fmt" - "go.pinniped.dev/internal/here" "testing" "time" @@ -17,6 +16,7 @@ import ( "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/here" "go.pinniped.dev/test/testlib" ) From 6d82a11645646a8c9a57990b6e20ef5ec5be7278 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 21 Jul 2023 12:30:40 -0700 Subject: [PATCH 57/81] CRD already validates that IDP transform constant names are unique - Remove that validation from the controller since the CRD already validates it during creates and updates. - Also finish the supervisor_federationdomain_status_test.go by adding more tests for both controller validations and CRD validations --- .../federation_domain_watcher.go | 48 +- .../federation_domain_watcher_test.go | 81 +-- ...supervisor_federationdomain_status_test.go | 664 +++++++++++++++++- test/testlib/client.go | 2 +- 4 files changed, 664 insertions(+), 131 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 6c467e923..7f9f16e8f 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -45,7 +45,6 @@ const ( typeIdentityProvidersDisplayNamesUnique = "IdentityProvidersDisplayNamesUnique" typeIdentityProvidersAPIGroupSuffixValid = "IdentityProvidersObjectRefAPIGroupSuffixValid" typeIdentityProvidersObjectRefKindValid = "IdentityProvidersObjectRefKindValid" - typeTransformsConstantsNamesUnique = "TransformsConstantsNamesUnique" typeTransformsExpressionsValid = "TransformsExpressionsValid" typeTransformsExamplesPassed = "TransformsExamplesPassed" @@ -62,7 +61,6 @@ const ( reasonDuplicateDisplayNames = "DuplicateDisplayNames" reasonAPIGroupNameUnrecognized = "APIGroupUnrecognized" reasonKindUnrecognized = "KindUnrecognized" - reasonDuplicateConstantsNames = "DuplicateConstantsNames" reasonInvalidTransformsExpressions = "InvalidTransformsExpressions" reasonTransformsExamplesFailed = "TransformsExamplesFailed" @@ -330,7 +328,6 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( conditions = appendIdentityProviderDuplicateDisplayNamesCondition(sets.Set[string]{}, conditions) conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, []string{}, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), []string{}, conditions) - conditions = appendTransformsConstantsNamesUniqueCondition([]string{}, conditions) conditions = appendTransformsExpressionsValidCondition([]string{}, conditions) conditions = appendTransformsExamplesPassedCondition([]string{}, conditions) @@ -431,7 +428,6 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplic conditions = appendIdentityProviderObjectRefAPIGroupSuffixCondition(c.apiGroup, badAPIGroupNames, conditions) conditions = appendIdentityProviderObjectRefKindCondition(c.sortedAllowedKinds(), badKinds, conditions) - conditions = appendTransformsConstantsNamesUniqueCondition(validationErrorMessages.errorsForConstants, conditions) conditions = appendTransformsExpressionsValidCondition(validationErrorMessages.errorsForExpressions, conditions) conditions = appendTransformsExamplesPassedCondition(validationErrorMessages.errorsForExamples, conditions) @@ -472,13 +468,10 @@ func (c *federationDomainWatcherController) makeTransformationPipelineAndEvaluat idpIndex int, validationErrorMessages *transformsValidationErrorMessages, ) (*idtransform.TransformationPipeline, bool, error) { - consts, errorsForConstants, err := c.makeTransformsConstantsForIdentityProvider(idp, idpIndex) + consts, err := c.makeTransformsConstantsForIdentityProvider(idp) if err != nil { return nil, false, err } - if len(errorsForConstants) > 0 { - validationErrorMessages.errorsForConstants = append(validationErrorMessages.errorsForConstants, errorsForConstants) - } pipeline, errorsForExpressions, err := c.makeTransformationPipelineForIdentityProvider(idp, idpIndex, consts) if err != nil { @@ -498,22 +491,17 @@ func (c *federationDomainWatcherController) makeTransformationPipelineAndEvaluat func (c *federationDomainWatcherController) makeTransformsConstantsForIdentityProvider( idp configv1alpha1.FederationDomainIdentityProvider, - idpIndex int, -) (*celtransformer.TransformationConstants, string, error) { +) (*celtransformer.TransformationConstants, error) { consts := &celtransformer.TransformationConstants{ StringConstants: map[string]string{}, StringListConstants: map[string][]string{}, } constNames := sets.Set[string]{} - duplicateConstNames := sets.Set[string]{} // Read all the declared constants. for _, constant := range idp.Transforms.Constants { // The CRD requires the name field, and validates that it has at least one character, - // so here we only need to validate that they are unique. - if constNames.Has(constant.Name) { - duplicateConstNames.Insert(constant.Name) - } + // and validates that the names are unique within the list. constNames.Insert(constant.Name) switch constant.Type { case "string": @@ -522,17 +510,11 @@ func (c *federationDomainWatcherController) makeTransformsConstantsForIdentityPr consts.StringListConstants[constant.Name] = constant.StringListValue default: // This shouldn't really happen since the CRD validates it, but handle it as an error. - return nil, "", fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) + return nil, fmt.Errorf("one of spec.identityProvider[].transforms.constants[].type is invalid: %q", constant.Type) } } - if duplicateConstNames.Len() > 0 { - return consts, fmt.Sprintf( - "the names specified by .spec.identityProviders[%d].transforms.constants[].name contain duplicates: %s", - idpIndex, strings.Join(sortAndQuote(duplicateConstNames.UnsortedList()), ", ")), nil - } - - return consts, "", nil + return consts, nil } func (c *federationDomainWatcherController) makeTransformationPipelineForIdentityProvider( @@ -764,25 +746,6 @@ func appendTransformsExamplesPassedCondition(messages []string, conditions []*co return conditions } -func appendTransformsConstantsNamesUniqueCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { - if len(messages) > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsConstantsNamesUnique, - Status: configv1alpha1.ConditionFalse, - Reason: reasonDuplicateConstantsNames, - Message: strings.Join(messages, "\n\n"), - }) - } else { - conditions = append(conditions, &configv1alpha1.Condition{ - Type: typeTransformsConstantsNamesUnique, - Status: configv1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", - }) - } - return conditions -} - func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { if duplicateDisplayNames.Len() > 0 { conditions = append(conditions, &configv1alpha1.Condition{ @@ -878,7 +841,6 @@ func (c *federationDomainWatcherController) sortedAllowedKinds() []string { } type transformsValidationErrorMessages struct { - errorsForConstants []string errorsForExpressions []string errorsForExamples []string } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index cfea69451..c3bbf433c 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -400,28 +400,6 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyConstNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "TransformsConstantsNamesUnique", - Status: "True", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "Success", - Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", - } - } - - sadConstNamesUniqueCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ - Type: "TransformsConstantsNamesUnique", - Status: "False", - ObservedGeneration: observedGeneration, - LastTransitionTime: time, - Reason: "DuplicateConstantsNames", - Message: errorMessages, - } - } - happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { return configv1alpha1.Condition{ Type: "TransformsExpressionsValid", @@ -537,7 +515,6 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { return sortConditionsByType([]configv1alpha1.Condition{ happyTransformationExamplesCondition(frozenMetav1Now, 123), happyTransformationExpressionsCondition(frozenMetav1Now, 123), - happyConstNamesUniqueCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), happyAPIGroupSuffixCondition(frozenMetav1Now, 123), happyDisplayNamesUniqueCondition(frozenMetav1Now, 123), @@ -1341,55 +1318,6 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, - { - name: "the federation domain has duplicate transformation const names", - inputObjects: []runtime.Object{ - oidcIdentityProvider, - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, - Spec: configv1alpha1.FederationDomainSpec{ - Issuer: "https://issuer1.com", - IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{ - { - DisplayName: "name1", - ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), - Kind: "OIDCIdentityProvider", - Name: oidcIdentityProvider.Name, - }, - Transforms: configv1alpha1.FederationDomainTransforms{ - Constants: []configv1alpha1.FederationDomainTransformsConstant{ - {Name: "duplicate1", Type: "string", StringValue: "abc"}, - {Name: "duplicate1", Type: "stringList", StringListValue: []string{"def"}}, - {Name: "duplicate1", Type: "string", StringValue: "efg"}, - {Name: "duplicate2", Type: "string", StringValue: "123"}, - {Name: "duplicate2", Type: "string", StringValue: "456"}, - {Name: "uniqueName", Type: "string", StringValue: "hij"}, - }, - }, - }, - }, - }, - }, - }, - wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, - wantStatusUpdates: []*configv1alpha1.FederationDomain{ - expectedFederationDomainStatusUpdate( - &configv1alpha1.FederationDomain{ - ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, - }, - configv1alpha1.FederationDomainPhaseError, - replaceConditions( - allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ - sadConstNamesUniqueCondition( - `the names specified by .spec.identityProviders[0].transforms.constants[].name contain duplicates: "duplicate1", "duplicate2"`, - frozenMetav1Now, 123), - sadReadyCondition(frozenMetav1Now, 123), - }), - ), - }, - }, { name: "the federation domain has transformation expressions which don't compile", inputObjects: []runtime.Object{ @@ -1707,7 +1635,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ {Name: "foo", Type: "string", StringValue: "bar"}, - {Name: "foo", Type: "string", StringValue: "baz"}, + {Name: "bar", Type: "string", StringValue: "baz"}, }, Expressions: []configv1alpha1.FederationDomainTransformsExpression{ {Type: "username/v1", Expression: `username + ":suffix"`}, @@ -1742,7 +1670,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Transforms: configv1alpha1.FederationDomainTransforms{ Constants: []configv1alpha1.FederationDomainTransformsConstant{ {Name: "foo", Type: "string", StringValue: "bar"}, - {Name: "foo", Type: "string", StringValue: "baz"}, + {Name: "bar", Type: "string", StringValue: "baz"}, }, Expressions: []configv1alpha1.FederationDomainTransformsExpression{ {Type: "username/v1", Expression: `username + ":suffix"`}, @@ -1821,11 +1749,6 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { replaceConditions( allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123), []configv1alpha1.Condition{ - sadConstNamesUniqueCondition(here.Doc( - `the names specified by .spec.identityProviders[0].transforms.constants[].name contain duplicates: "foo" - - the names specified by .spec.identityProviders[1].transforms.constants[].name contain duplicates: "foo"`, - ), frozenMetav1Now, 123), sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123), sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index d426d814a..83a465e22 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" @@ -23,12 +24,13 @@ import ( // Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go. func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { env := testlib.IntegrationEnv(t) - client := testlib.NewSupervisorClientset(t) + supervisorClient := testlib.NewSupervisorClientset(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() + t.Cleanup(cancel) - temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret(ctx, t, env.SupervisorNamespace, defaultTLSCertSecretName(env), client, testlib.NewKubernetesClientset(t)) + temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret(ctx, t, + env.SupervisorNamespace, defaultTLSCertSecretName(env), supervisorClient, testlib.NewKubernetesClientset(t)) tests := []struct { name string @@ -164,7 +166,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { allSuccessfulFederationDomainConditions(fd.Spec)) // Removing one IDP should put the FederationDomain back into an error status again. - oidcIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().OIDCIdentityProviders(env.SupervisorNamespace) + oidcIDPClient := supervisorClient.IDPV1alpha1().OIDCIdentityProviders(env.SupervisorNamespace) err := oidcIDPClient.Delete(ctx, oidcIdentityProvider1.Name, metav1.DeleteOptions{}) require.NoError(t, err) testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) @@ -183,6 +185,347 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { )) }, }, + { + name: "spec with explicit identity providers and lots of validation errors", + run: func(t *testing.T) { + oidcIdentityProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", + Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, + }, idpv1alpha1.PhaseError) + + fd := testlib.CreateTestFederationDomain(ctx, t, v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com/fake", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "not unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("this is the wrong api group"), + Kind: "OIDCIdentityProvider", + Name: "will not be found", + }, + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string", StringValue: "bar"}, + }, + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: "this is not a valid cel expression"}, + {Type: "groups/v1", Expression: "this is also not a valid cel expression"}, + {Type: "username/v1", Expression: "username"}, // valid + {Type: "policy/v1", Expression: "still not a valid cel expression"}, + }, + Examples: []v1alpha1.FederationDomainTransformsExample{ + { + Username: "does not matter because expressions did not compile", + }, + }, + }, + }, + { // this identity provider should be valid + DisplayName: "unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + }, + { + DisplayName: "not unique", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "this is the wrong kind", + Name: "also will not be found", + }, + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "ryan", Type: "string", StringValue: "ryan"}, + {Name: "unused", Type: "stringList", StringListValue: []string{"foo", "bar"}}, + {Name: "rejectMe", Type: "string", StringValue: "rejectMeWithDefaultMessage"}, + }, + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + {Type: "policy/v1", Expression: `username == strConst.ryan || username == strConst.rejectMe`, Message: "only special users allowed"}, + {Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified + {Type: "username/v1", Expression: `"pre:" + username`}, + {Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`}, + }, + Examples: []v1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"pre:b", "pre:a", "pre:b", "pre:a"}, // order and repeats don't matter, treated like a set + Rejected: false, + }, + }, + { // this example should pass + Username: "other", + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only special users allowed", + }, + }, + { // this example should fail because it expects the user to be rejected but the user was actually not rejected + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "this input is ignored in this case", + }, + }, + { // this example should fail because it expects the user not to be rejected but they were actually rejected + Username: "other", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:other", + Groups: []string{"pre:a", "pre:b"}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong rejection message + Username: "other", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "wrong message", + }, + }, + { // this example should pass even though it does not make any assertion about the rejection message + // because the message assertions defaults to asserting the default rejection message + Username: "rejectMeWithDefaultMessage", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + }, + }, + { // this example should fail because it expects both the wrong username and groups + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong username only + Username: "ryan", + Groups: []string{"a", "b"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{"pre:b", "pre:a"}, + Rejected: false, + }, + }, + { // this example should fail because it expects the wrong groups only + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"wrong2", "wrong1"}, + Rejected: false, + }, + }, + { // this example should fail because it does not expect anything but the auth actually was successful + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{}, + }, + }, + }, + }, + }, + }, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulFederationDomainConditions(fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersDisplayNamesUnique", Status: "False", Reason: "DuplicateDisplayNames", + Message: `the names specified by .spec.identityProviders[].displayName contain duplicates: "not unique"`, + }, + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", + Message: here.Doc( + `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "will not be found") + + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "also will not be found")`, + )}, + { + Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "False", Reason: "APIGroupUnrecognized", + Message: `some API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized ` + + `(should be "idp.supervisor.pinniped.dev"): "this is the wrong api group"`, + }, + { + Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized", + Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` + + `(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + { + Type: "TransformsExamplesPassed", Status: "False", Reason: "TransformsExamplesFailed", + Message: here.Doc( + `unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid + + .spec.identityProviders[2].transforms.examples[2] example failed: + expected: authentication to be rejected + actual: authentication was not rejected + + .spec.identityProviders[2].transforms.examples[3] example failed: + expected: authentication not to be rejected + actual: authentication was rejected with message "only special users allowed" + + .spec.identityProviders[2].transforms.examples[4] example failed: + expected: authentication rejection message "wrong message" + actual: authentication rejection message "only special users allowed" + + .spec.identityProviders[2].transforms.examples[6] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[2].transforms.examples[6] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[2].transforms.examples[7] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[2].transforms.examples[8] example failed: + expected: groups ["wrong1", "wrong2"] + actual: groups ["pre:a", "pre:b"] + + .spec.identityProviders[2].transforms.examples[9] example failed: + expected: username "" + actual: username "pre:ryan" + + .spec.identityProviders[2].transforms.examples[9] example failed: + expected: groups [] + actual: groups ["pre:a", "pre:b"]`, + ), + }, + { + Type: "TransformsExpressionsValid", Status: "False", Reason: "InvalidTransformsExpressions", + Message: here.Doc( + `spec.identityProvider[0].transforms.expressions[0].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[1].expression was invalid: + CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting + | this is also not a valid cel expression + | .....^ + + spec.identityProvider[0].transforms.expressions[3].expression was invalid: + CEL expression compile error: ERROR: :1:7: Syntax error: mismatched input 'not' expecting + | still not a valid cel expression + | ......^`, + ), + }, + }, + )) + + // Updating the FederationDomain to fix some of the problems should make some of the errors go away. + federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) + fd, err := federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) + require.NoError(t, err) + fd.Spec.IdentityProviders[0] = v1alpha1.FederationDomainIdentityProvider{ + // Fix the display name. + DisplayName: "now made unique", + // Fix the objectRef. + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string", StringValue: "bar"}, + }, + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + // Fix the compile errors. + {Type: "username/v1", Expression: `"pre:" + username`}, + }, + Examples: []v1alpha1.FederationDomainTransformsExample{ + { // this example should fail because it expects both the wrong username and groups + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{}, + Rejected: false, + }, + }, + }, + }, + } + fd.Spec.IdentityProviders[2].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "other", + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only special users allowed", + }, + }, + } + fd, err = federationDomainsClient.Update(ctx, fd, metav1.UpdateOptions{}) + require.NoError(t, err) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( + allSuccessfulFederationDomainConditions(fd.Spec), + []v1alpha1.Condition{ + { + Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", + Message: `cannot find resource specified by .spec.identityProviders[2].objectRef (with name "also will not be found")`, + }, + { + Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized", + Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` + + `(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, + }, + { + Type: "Ready", Status: "False", Reason: "NotReady", + Message: "the FederationDomain is not ready: see other conditions for details", + }, + { + Type: "TransformsExamplesPassed", Status: "False", Reason: "TransformsExamplesFailed", + Message: here.Doc( + `.spec.identityProviders[0].transforms.examples[0] example failed: + expected: username "wrong" + actual: username "pre:ryan" + + .spec.identityProviders[0].transforms.examples[0] example failed: + expected: groups [] + actual: groups ["a", "b"]`, + ), + }, + }, + )) + + // Updating the FederationDomain to fix the rest of the problems should make all the errors go away. + fd, err = federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) + require.NoError(t, err) + fd.Spec.IdentityProviders[2].ObjectRef = corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + } + fd.Spec.IdentityProviders[0].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"a", "b"}, + }, + }, + } + fd, err = federationDomainsClient.Update(ctx, fd, metav1.UpdateOptions{}) + require.NoError(t, err) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, allSuccessfulFederationDomainConditions(fd.Spec)) + }, + }, } for _, test := range tests { @@ -193,6 +536,315 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { } } +func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + fdClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + objectMeta := testlib.ObjectMetaWithRandomName(t, "federation-domain") + + tests := []struct { + name string + fd *v1alpha1.FederationDomain + wantErr string + }{ + { + name: "issuer cannot be empty", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "", + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.issuer: Invalid value: "": spec.issuer in body should be at least 1 chars long`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP display names cannot be empty", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{{DisplayName: ""}}, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].displayName: Invalid value: "": `+ + "spec.identityProviders[0].displayName in body should be at least 1 chars long", + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform constants must have unique names", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "notUnique", Type: "string", StringValue: "foo"}, + {Name: "notUnique", Type: "string", StringValue: "bar"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.constants[1]: Duplicate value: map[string]interface {}{"name":"notUnique"}`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform constant names cannot be empty", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "", Type: "string"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.constants[0].name: Invalid value: "": `+ + `spec.identityProviders[0].transforms.constants[0].name in body should be at least 1 chars long`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform constant names cannot be more than 64 characters", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "12345678901234567890123456789012345678901234567890123456789012345", Type: "string"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.constants[0].name: Too long: may not be longer than 64`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform constant names must be a legal CEL variable name", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "cannot have spaces", Type: "string"}, + {Name: "1mustStartWithLetter", Type: "string"}, + {Name: "_mustStartWithLetter", Type: "string"}, + {Name: "canOnlyIncludeLettersAndNumbersAnd_", Type: "string"}, + {Name: "CanStart1_withUpperCase", Type: "string"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `[spec.identityProviders[0].transforms.constants[0].name: Invalid value: "cannot have spaces": `+ + `spec.identityProviders[0].transforms.constants[0].name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$', `+ + `spec.identityProviders[0].transforms.constants[1].name: Invalid value: "1mustStartWithLetter": `+ + `spec.identityProviders[0].transforms.constants[1].name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$', `+ + `spec.identityProviders[0].transforms.constants[2].name: Invalid value: "_mustStartWithLetter": `+ + `spec.identityProviders[0].transforms.constants[2].name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$']`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform constant types must be one of the allowed enum strings", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "a", Type: "this is invalid"}, + {Name: "b", Type: "string"}, + {Name: "c", Type: "stringList"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.constants[0].type: Unsupported value: "this is invalid": `+ + `supported values: "string", "stringList"`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform expression types must be one of the allowed enum strings", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + {Type: "this is invalid", Expression: "foo"}, + {Type: "policy/v1", Expression: "foo"}, + {Type: "username/v1", Expression: "foo"}, + {Type: "groups/v1", Expression: "foo"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.expressions[0].type: Unsupported value: "this is invalid": `+ + `supported values: "policy/v1", "username/v1", "groups/v1"`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform expressions cannot be empty", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: ""}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.expressions[0].expression: Invalid value: "": `+ + `spec.identityProviders[0].transforms.expressions[0].expression in body should be at least 1 chars long`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "IDP transform example usernames cannot be empty", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: objectMeta, + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Examples: []v1alpha1.FederationDomainTransformsExample{ + {Username: ""}, + {Username: "non-empty"}, + }, + }, + }, + }, + }, + }, + wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.examples[0].username: Invalid value: "": `+ + `spec.identityProviders[0].transforms.examples[0].username in body should be at least 1 chars long`, + env.APIGroupSuffix, objectMeta.Name), + }, + { + name: "minimum valid", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "fd"), + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + }, + }, + }, + { + name: "minimum valid when IDPs are included", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "fd"), + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + {DisplayName: "foo"}, + }, + }, + }, + }, + { + name: "minimum valid when IDP has transform constants, expressions, and examples", + fd: &v1alpha1.FederationDomain{ + ObjectMeta: testlib.ObjectMetaWithRandomName(t, "fd"), + Spec: v1alpha1.FederationDomainSpec{ + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "foo", + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string"}, + }, + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + {Type: "username/v1", Expression: "foo"}, + }, + Examples: []v1alpha1.FederationDomainTransformsExample{ + {Username: "foo"}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, createErr := fdClient.Create(ctx, tt.fd, metav1.CreateOptions{}) + + t.Cleanup(func() { + // Delete it if it exists. + delErr := fdClient.Delete(ctx, tt.fd.Name, metav1.DeleteOptions{}) + if !k8serrors.IsNotFound(delErr) { + require.NoError(t, delErr) + } + }) + + if tt.wantErr == "" { + require.NoError(t, createErr) + } else { + require.EqualError(t, createErr, tt.wantErr) + } + }) + } +} + func replaceSomeConditions(conditions []v1alpha1.Condition, replaceWithTheseConditions []v1alpha1.Condition) []v1alpha1.Condition { cp := make([]v1alpha1.Condition, len(conditions)) copy(cp, conditions) @@ -257,10 +909,6 @@ func allSuccessfulFederationDomainConditions(federationDomainSpec v1alpha1.Feder Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+ "the discovery endpoint is %s/.well-known/openid-configuration", federationDomainSpec.Issuer), }, - { - Type: "TransformsConstantsNamesUnique", Status: "True", Reason: "Success", - Message: "the names specified by .spec.identityProviders[].transforms.constants[].name are unique", - }, { Type: "TransformsExamplesPassed", Status: "True", Reason: "Success", Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", diff --git a/test/testlib/client.go b/test/testlib/client.go index 6053a671d..c80c2e20d 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -354,7 +354,7 @@ func WaitForFederationDomainStatusConditions(ctx context.Context, t *testing.T, "wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d", expectConditions, fd.Status.Conditions, i) } - }, 5*time.Second, 1*time.Second, "wanted FederationDomain conditions") + }, 60*time.Second, 1*time.Second, "wanted FederationDomain conditions") } func RandBytes(t *testing.T, numBytes int) []byte { From 446384a7f5095e7b4f47641cd993d0577e8d1175 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 21 Jul 2023 14:55:02 -0700 Subject: [PATCH 58/81] add an e2e test for a FederationDomain with multiple IDPs and transforms --- test/integration/e2e_test.go | 247 ++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index dd3741784..0169b7df6 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -30,6 +30,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/pointer" authv1alpha "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -1173,6 +1174,250 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) + + t.Run("with multiple IDPs: one OIDC and one LDAP", func(t *testing.T) { + testlib.SkipTestWhenLDAPIsUnavailable(t, env) + + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. + page := browsertest.Open(t) + + downstreamPrefix := "pre:" + + expectedUpstreamLDAPUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedDownstreamLDAPUsername := downstreamPrefix + expectedUpstreamLDAPUsername + expectedUpstreamLDAPGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + expectedDownstreamLDAPGroups := make([]string, 0, len(expectedUpstreamLDAPGroups)) + for _, g := range expectedUpstreamLDAPGroups { + expectedDownstreamLDAPGroups = append(expectedDownstreamLDAPGroups, downstreamPrefix+g) + } + + expectedUpstreamOIDCUsername := env.SupervisorUpstreamOIDC.Username + expectedDownstreamOIDCUsername := downstreamPrefix + expectedUpstreamOIDCUsername + expectedUpstreamOIDCGroups := env.SupervisorUpstreamOIDC.ExpectedGroups + expectedDownstreamOIDCGroups := make([]string, 0, len(expectedUpstreamOIDCGroups)) + for _, g := range expectedUpstreamOIDCGroups { + expectedDownstreamOIDCGroups = append(expectedDownstreamOIDCGroups, downstreamPrefix+g) + } + + createdLDAPProvider := setupClusterForEndToEndLDAPTest(t, expectedDownstreamLDAPUsername, env) + + // Having one IDP should put the FederationDomain into a ready state. + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + + // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. + testlib.CreateTestClusterRoleBinding(t, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedDownstreamOIDCUsername}, + rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, + ) + testlib.WaitForUserToHaveAccess(t, expectedDownstreamOIDCUsername, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + // Create upstream OIDC provider and wait for it to become ready. + createdOIDCProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + }, + Claims: idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + }, + }, idpv1alpha1.PhaseReady) + + // Having a second IDP should put the FederationDomain back into an error state until we tell it which one to use. + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseError) + + // Update the FederationDomain to use the two IDPs. + federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) + gotFederationDomain, err := federationDomainsClient.Get(testCtx, downstream.Name, metav1.GetOptions{}) + require.NoError(t, err) + + ldapIDPDisplayName := "My LDAP IDP 💾" + oidcIDPDisplayName := "My OIDC IDP 🚀" + + gotFederationDomain.Spec.IdentityProviders = []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: ldapIDPDisplayName, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "LDAPIdentityProvider", + Name: createdLDAPProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + {Name: "allowedUser", Type: "string", StringValue: expectedUpstreamLDAPUsername}, + {Name: "allowedUsers", Type: "stringList", StringListValue: []string{"someone else", expectedUpstreamLDAPUsername, "someone else"}}, + }, + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "policy/v1", Expression: `username == strConst.allowedUser && username in strListConst.allowedUsers`, Message: "only special users allowed"}, + {Type: "username/v1", Expression: fmt.Sprintf(`"%s" + username`, downstreamPrefix)}, + {Type: "groups/v1", Expression: fmt.Sprintf(`groups.map(g, "%s" + g)`, downstreamPrefix)}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { + Username: expectedUpstreamLDAPUsername, + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: expectedDownstreamLDAPUsername, + Groups: []string{downstreamPrefix + "a", downstreamPrefix + "b"}, + }, + }, + { + Username: "someone other user", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only special users allowed", + }, + }, + }, + }, + }, + { + DisplayName: oidcIDPDisplayName, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: createdOIDCProvider.Name, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Constants: []configv1alpha1.FederationDomainTransformsConstant{ + {Name: "allowedUser", Type: "string", StringValue: expectedUpstreamOIDCUsername}, + {Name: "allowedUsers", Type: "stringList", StringListValue: []string{"someone else", expectedUpstreamOIDCUsername, "someone else"}}, + }, + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + {Type: "policy/v1", Expression: `username == strConst.allowedUser && username in strListConst.allowedUsers`, Message: "only special users allowed"}, + {Type: "username/v1", Expression: fmt.Sprintf(`"%s" + username`, downstreamPrefix)}, + {Type: "groups/v1", Expression: fmt.Sprintf(`groups.map(g, "%s" + g)`, downstreamPrefix)}, + }, + Examples: []configv1alpha1.FederationDomainTransformsExample{ + { + Username: expectedUpstreamOIDCUsername, + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Username: expectedDownstreamOIDCUsername, + Groups: []string{downstreamPrefix + "a", downstreamPrefix + "b"}, + }, + }, + { + Username: "someone other user", + Groups: []string{"a", "b"}, + Expects: configv1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only special users allowed", + }, + }, + }, + }, + }, + } + _, err = federationDomainsClient.Update(testCtx, gotFederationDomain, metav1.UpdateOptions{}) + require.NoError(t, err) + + // The FederationDomain should be valid after the above update. + testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/test-sessions.yaml" + + ldapKubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-session-cache", sessionCachePath, + "--upstream-identity-provider-name", ldapIDPDisplayName, + // use default for --oidc-scopes, which is to request all relevant scopes + }) + + oidcKubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-session-cache", sessionCachePath, + "--upstream-identity-provider-name", oidcIDPDisplayName, + // use default for --oidc-scopes, which is to request all relevant scopes + }) + + // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin for the LDAP IDP. + t.Log("starting LDAP auth via kubectl") + timeoutCtx, cleanupTimeoutCtx := context.WithTimeout(testCtx, 10*time.Second) + t.Cleanup(cleanupTimeoutCtx) + start := time.Now() + kubectlCmd := exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + ptyFile, err := pty.Start(kubectlCmd) + require.NoError(t, err) + + // Wait for the subprocess to print the username prompt, then type the user's username. + readFromFileUntilStringIsSeen(t, ptyFile, "Username: ") + _, err = ptyFile.WriteString(expectedUpstreamLDAPUsername + "\n") + require.NoError(t, err) + + // Wait for the subprocess to print the password prompt, then type the user's password. + readFromFileUntilStringIsSeen(t, ptyFile, "Password: ") + _, err = ptyFile.WriteString(env.SupervisorUpstreamLDAP.TestUserPassword + "\n") + require.NoError(t, err) + + // Read all output from the subprocess until EOF. + // Ignore any errors returned because there is always an error on linux. + kubectlOutputBytes, _ := io.ReadAll(ptyFile) + requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes)) + + t.Logf("first kubectl command took %s", time.Since(start).String()) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, ldapIDPDisplayName, ldapKubeconfigPath, + sessionCachePath, pinnipedExe, expectedDownstreamLDAPUsername, expectedDownstreamLDAPGroups, allScopes) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin for the OIDC IDP. + t.Log("starting OIDC auth via kubectl") + timeoutCtx, cleanupTimeoutCtx = context.WithTimeout(testCtx, 10*time.Second) + t.Cleanup(cleanupTimeoutCtx) + kubectlCmd = exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", oidcKubeconfigPath, "-v", "6") + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) + + // Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form. + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) + + // Expect to be redirected to the downstream callback which is serving the form_post HTML. + t.Logf("waiting for response page %s", downstream.Spec.Issuer) + browsertest.WaitForURL(t, page, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + + // The response page should have done the background fetch() and POST'ed to the CLI's callback. + // It should now be in the "success" state. + formpostExpectSuccessState(t, page) + + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) + + // The user is now logged in to the cluster as two different identities simultaneously, and can switch + // back and forth by switching kubeconfigs, without needing to auth again. + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, oidcIDPDisplayName, oidcKubeconfigPath, + sessionCachePath, pinnipedExe, expectedDownstreamOIDCUsername, expectedDownstreamOIDCGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, ldapIDPDisplayName, ldapKubeconfigPath, + sessionCachePath, pinnipedExe, expectedDownstreamLDAPUsername, expectedDownstreamLDAPGroups, allScopes) + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, oidcIDPDisplayName, oidcKubeconfigPath, + sessionCachePath, pinnipedExe, expectedDownstreamOIDCUsername, expectedDownstreamOIDCGroups, allScopes) + }) } func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *testing.T, kubectlCmd *exec.Cmd, b *browsertest.Browser) chan string { @@ -1527,7 +1772,7 @@ func runPinnipedGetKubeconfig(t *testing.T, env *testlib.TestEnv, pinnipedExe st require.NotNil(t, restConfig.ExecProvider) require.Equal(t, []string{"login", "oidc"}, restConfig.ExecProvider.Args[:2]) - kubeconfigPath := filepath.Join(tempDir, "kubeconfig.yaml") + kubeconfigPath := filepath.Join(tempDir, fmt.Sprintf("kubeconfig-%s.yaml", testlib.RandHex(t, 8))) require.NoError(t, os.WriteFile(kubeconfigPath, []byte(kubeconfigYAML), 0600)) return kubeconfigPath From 92bf826ec516456b83cdec6e126201c4b8d35c59 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 21 Jul 2023 14:59:40 -0700 Subject: [PATCH 59/81] rename a local variable in an integration test --- test/integration/e2e_test.go | 108 +++++++++++++++++------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 0169b7df6..50c632f6d 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -101,7 +101,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { ) // Create the downstream FederationDomain and expect it to go into the success status condition. - downstream := testlib.CreateTestFederationDomain(topSetupCtx, t, + federationDomain := testlib.CreateTestFederationDomain(topSetupCtx, t, configv1alpha1.FederationDomainSpec{ Issuer: issuerURL.String(), TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, @@ -112,7 +112,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. clusterAudience := "test-cluster-" + testlib.RandHex(t, 8) authenticator := testlib.CreateTestJWTAuthenticator(topSetupCtx, t, authv1alpha.JWTAuthenticatorSpec{ - Issuer: downstream.Spec.Issuer, + Issuer: federationDomain.Spec.Issuer, Audience: clusterAudience, TLS: &authv1alpha.TLSSpec{CertificateAuthorityData: testCABundleBase64}, }) @@ -159,7 +159,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -186,8 +186,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. - t.Logf("waiting for response page %s", downstream.Spec.Issuer) - browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) // The response page should have done the background fetch() and POST'ed to the CLI's callback. // It should now be in the "success" state. @@ -195,7 +195,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -242,7 +242,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -269,8 +269,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. - t.Logf("waiting for response page %s", downstream.Spec.Issuer) - browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) // The response page should have done the background fetch() and POST'ed to the CLI's callback. // It should now be in the "success" state. @@ -282,7 +282,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted // scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will // assert that the expected username and groups claims/values are in the downstream ID token. - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) }) @@ -327,7 +327,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -375,8 +375,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. - t.Logf("waiting for response page %s", downstream.Spec.Issuer) - browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) // The response page should have failed to automatically post, and should now be showing the manual instructions. authCode := formpostExpectManualState(t, browser) @@ -395,7 +395,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -448,7 +448,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -502,8 +502,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. - t.Logf("waiting for response page %s", downstream.Spec.Issuer) - browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) // The response page should have failed to automatically post, and should now be showing the manual instructions. authCode := formpostExpectManualState(t, browser) @@ -533,7 +533,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -576,7 +576,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -618,7 +618,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -646,7 +646,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -719,7 +719,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -757,7 +757,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -775,7 +775,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -817,7 +817,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted // scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will // assert that the expected username and groups claims/values are in the downstream ID token. - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"}) }) @@ -835,7 +835,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -885,7 +885,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -903,7 +903,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -941,7 +941,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -959,7 +959,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1009,7 +1009,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -1029,7 +1029,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1054,14 +1054,14 @@ func TestE2EFullIntegration_Browser(t *testing.T) { kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser) // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. - browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer, + browsertest.LoginToUpstreamLDAP(t, browser, federationDomain.Spec.Issuer, expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword) formpostExpectSuccessState(t, browser) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -1081,7 +1081,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1106,14 +1106,14 @@ func TestE2EFullIntegration_Browser(t *testing.T) { kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser) // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. - browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer, + browsertest.LoginToUpstreamLDAP(t, browser, federationDomain.Spec.Issuer, expectedUsername, env.SupervisorUpstreamActiveDirectory.TestUserPassword) formpostExpectSuccessState(t, browser) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -1133,7 +1133,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env) - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1164,14 +1164,14 @@ func TestE2EFullIntegration_Browser(t *testing.T) { kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser) // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. - browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer, + browsertest.LoginToUpstreamLDAP(t, browser, federationDomain.Spec.Issuer, expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword) formpostExpectSuccessState(t, browser) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, createdProvider.Name, kubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) @@ -1184,7 +1184,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. - page := browsertest.Open(t) + browser := browsertest.OpenBrowser(t) downstreamPrefix := "pre:" @@ -1207,7 +1207,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { createdLDAPProvider := setupClusterForEndToEndLDAPTest(t, expectedDownstreamLDAPUsername, env) // Having one IDP should put the FederationDomain into a ready state. - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. testlib.CreateTestClusterRoleBinding(t, @@ -1240,11 +1240,11 @@ func TestE2EFullIntegration_Browser(t *testing.T) { }, idpv1alpha1.PhaseReady) // Having a second IDP should put the FederationDomain back into an error state until we tell it which one to use. - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseError) // Update the FederationDomain to use the two IDPs. federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) - gotFederationDomain, err := federationDomainsClient.Get(testCtx, downstream.Name, metav1.GetOptions{}) + gotFederationDomain, err := federationDomainsClient.Get(testCtx, federationDomain.Name, metav1.GetOptions{}) require.NoError(t, err) ldapIDPDisplayName := "My LDAP IDP 💾" @@ -1330,7 +1330,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { require.NoError(t, err) // The FederationDomain should be valid after the above update. - testlib.WaitForFederationDomainStatusPhase(testCtx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" @@ -1383,7 +1383,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, ldapIDPDisplayName, ldapKubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, ldapIDPDisplayName, ldapKubeconfigPath, sessionCachePath, pinnipedExe, expectedDownstreamLDAPUsername, expectedDownstreamLDAPGroups, allScopes) // Run "kubectl get namespaces" which should trigger a browser login via the plugin for the OIDC IDP. @@ -1394,28 +1394,28 @@ func TestE2EFullIntegration_Browser(t *testing.T) { kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. - kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser) // Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form. - browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. - t.Logf("waiting for response page %s", downstream.Spec.Issuer) - browsertest.WaitForURL(t, page, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer))) + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) // The response page should have done the background fetch() and POST'ed to the CLI's callback. // It should now be in the "success" state. - formpostExpectSuccessState(t, page) + formpostExpectSuccessState(t, browser) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) // The user is now logged in to the cluster as two different identities simultaneously, and can switch // back and forth by switching kubeconfigs, without needing to auth again. - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, oidcIDPDisplayName, oidcKubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, oidcIDPDisplayName, oidcKubeconfigPath, sessionCachePath, pinnipedExe, expectedDownstreamOIDCUsername, expectedDownstreamOIDCGroups, allScopes) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, ldapIDPDisplayName, ldapKubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, ldapIDPDisplayName, ldapKubeconfigPath, sessionCachePath, pinnipedExe, expectedDownstreamLDAPUsername, expectedDownstreamLDAPGroups, allScopes) - requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, oidcIDPDisplayName, oidcKubeconfigPath, + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, oidcIDPDisplayName, oidcKubeconfigPath, sessionCachePath, pinnipedExe, expectedDownstreamOIDCUsername, expectedDownstreamOIDCGroups, allScopes) }) } From c701a4a344c4cb43826eac46ac42056d8239bb16 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 21 Jul 2023 15:09:03 -0700 Subject: [PATCH 60/81] remove expectation about TransformsConstantsNamesUnique status condition Forgot to remove this in the previous commit which removed writing that condition from the controller code. --- test/integration/supervisor_discovery_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 3d8cc2d75..b6a84b889 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -668,7 +668,6 @@ func withAllSuccessfulConditions() map[string]v1alpha1.ConditionStatus { "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, - "TransformsConstantsNamesUnique": v1alpha1.ConditionTrue, "TransformsExpressionsValid": v1alpha1.ConditionTrue, "TransformsExamplesPassed": v1alpha1.ConditionTrue, } From 957892b67766cdb9f34e1cb5342f58ede1a75e0a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 25 Jul 2023 14:32:45 -0700 Subject: [PATCH 61/81] handle old versions of k8s in supervisor_federationdomain_status_test.go --- ...supervisor_federationdomain_status_test.go | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index 83a465e22..333defe4a 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -567,8 +567,15 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { fd: &v1alpha1.FederationDomain{ ObjectMeta: objectMeta, Spec: v1alpha1.FederationDomainSpec{ - Issuer: "https://example.com", - IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{{DisplayName: ""}}, + Issuer: "https://example.com", + IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: "", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, + }, + }, }, }, wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ @@ -585,6 +592,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "notUnique", Type: "string", StringValue: "foo"}, @@ -608,6 +618,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "", Type: "string"}, @@ -631,6 +644,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "12345678901234567890123456789012345678901234567890123456789012345", Type: "string"}, @@ -653,6 +669,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "cannot have spaces", Type: "string"}, @@ -684,6 +703,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "a", Type: "this is invalid"}, @@ -709,6 +731,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Expressions: []v1alpha1.FederationDomainTransformsExpression{ {Type: "this is invalid", Expression: "foo"}, @@ -735,6 +760,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Expressions: []v1alpha1.FederationDomainTransformsExpression{ {Type: "username/v1", Expression: ""}, @@ -758,6 +786,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Examples: []v1alpha1.FederationDomainTransformsExample{ {Username: ""}, @@ -789,7 +820,12 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { Spec: v1alpha1.FederationDomainSpec{ Issuer: "https://example.com", IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ - {DisplayName: "foo"}, + { + DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, + }, }, }, }, @@ -803,6 +839,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { IdentityProviders: []v1alpha1.FederationDomainIdentityProvider{ { DisplayName: "foo", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ {Name: "foo", Type: "string"}, From 01ab7758d8ce877b830ef643cc997823e6926ce5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 25 Jul 2023 16:18:15 -0700 Subject: [PATCH 62/81] Add e2e test for rejecting auth using identity transformation policy --- test/integration/e2e_test.go | 114 +++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 50c632f6d..5f2c9604d 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/pointer" @@ -163,6 +164,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -172,6 +174,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-browser", "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -246,6 +249,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -255,6 +259,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-browser", "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, "--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups }) @@ -331,6 +336,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -341,6 +347,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-listen", "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -452,6 +459,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -462,6 +470,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-listen", "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -580,6 +589,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -591,6 +601,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -650,6 +661,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -667,6 +679,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--upstream-identity-provider-flow", "cli_password", "--oidc-ca-bundle", testCABundlePath, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -723,6 +736,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -730,6 +744,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -779,6 +794,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -786,6 +802,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, "--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups }) @@ -839,6 +856,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -846,6 +864,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -907,6 +926,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -914,6 +934,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -963,6 +984,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -970,6 +992,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1033,6 +1056,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -1043,6 +1067,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-ca-bundle", testCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1085,6 +1110,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -1095,6 +1121,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-ca-bundle", testCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1137,6 +1164,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -1147,6 +1175,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-ca-bundle", testCABundlePath, "--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, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1334,6 +1363,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Use a specific session cache for this test. sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" ldapKubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -1341,6 +1371,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, "--upstream-identity-provider-name", ldapIDPDisplayName, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1352,6 +1383,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, "--upstream-identity-provider-name", oidcIDPDisplayName, // use default for --oidc-scopes, which is to request all relevant scopes }) @@ -1417,6 +1449,88 @@ func TestE2EFullIntegration_Browser(t *testing.T) { sessionCachePath, pinnipedExe, expectedDownstreamLDAPUsername, expectedDownstreamLDAPGroups, allScopes) requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, oidcIDPDisplayName, oidcKubeconfigPath, sessionCachePath, pinnipedExe, expectedDownstreamOIDCUsername, expectedDownstreamOIDCGroups, allScopes) + + // Update the policies of both IDPs on the FederationDomain to reject the expected upstream usernames during authentication. + // Remove the examples since we are changing the transforms. + _, err = federationDomainsClient.Patch(testCtx, gotFederationDomain.Name, types.JSONPatchType, + []byte(here.Doc( + `[ + { + "op": "replace", + "path": "/spec/identityProviders/0/transforms/expressions/0", + "value": { + "type": "policy/v1", + "expression": "username != strConst.allowedUser", + "message": "only special LDAP users allowed" + } + }, + { + "op": "replace", + "path": "/spec/identityProviders/1/transforms/expressions/0", + "value": { + "type": "policy/v1", + "expression": "username != strConst.allowedUser", + "message": "only special OIDC users allowed" + } + }, + { + "op": "remove", + "path": "/spec/identityProviders/0/transforms/examples" + }, + { + "op": "remove", + "path": "/spec/identityProviders/1/transforms/examples" + } + ]`, + )), + metav1.PatchOptions{}, + ) + require.NoError(t, err) + + // Wait for the status conditions to have observed the current spec generation so we can be sure that the + // controller has observed our latest update. + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + fd, err := federationDomainsClient.Get(testCtx, federationDomain.Name, metav1.GetOptions{}) + require.NoError(t, err) + t.Log("saw FederationDomain", fd) + requireEventually.Equal(fd.Generation, fd.Status.Conditions[0].ObservedGeneration) + }, 20*time.Second, 250*time.Millisecond) + // The FederationDomain should be valid after the above update. + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) + + // Log out so we can try fresh logins again. + require.NoError(t, os.Remove(credentialCachePath)) + require.NoError(t, os.Remove(sessionCachePath)) + + // Policies don't impact the kubeconfig files, so we can reuse the existing kubeconfig files. + // Try to log again, and this time expect to be rejected by the configured policies. + + // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin for the LDAP IDP. + t.Log("starting second LDAP auth via kubectl") + kubectlCmd = exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + ptyFile, err = pty.Start(kubectlCmd) + require.NoError(t, err) + + // Wait for the subprocess to print the username prompt, then type the user's username. + readFromFileUntilStringIsSeen(t, ptyFile, "Username: ") + _, err = ptyFile.WriteString(expectedUpstreamLDAPUsername + "\n") + require.NoError(t, err) + + // Wait for the subprocess to print the password prompt, then type the user's password. + readFromFileUntilStringIsSeen(t, ptyFile, "Password: ") + _, err = ptyFile.WriteString(env.SupervisorUpstreamLDAP.TestUserPassword + "\n") + require.NoError(t, err) + + // Read all output from the subprocess until EOF. + // Ignore any errors returned because there is always an error on linux. + kubectlOutputBytes, _ = io.ReadAll(ptyFile) + t.Log("kubectl command output:\n", string(kubectlOutputBytes)) + require.Contains(t, string(kubectlOutputBytes), + `Error: could not complete Pinniped login: login failed with code "access_denied": `+ + `The resource owner or authorization server denied the request. `+ + `Reason: configured identity policy rejected this authentication: only special LDAP users allowed.`) + require.Contains(t, string(kubectlOutputBytes), "pinniped failed with exit code 1") }) } From e6c78facfc83de4f460c5fe48f1faf60c0581cc3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 26 Jul 2023 09:45:37 -0700 Subject: [PATCH 63/81] Fix expectations in FederationDomains status test for old Kube versions Also try to avoid flakes by using RetryOnConflict when calling Update on the FederationDomain. --- test/integration/e2e_test.go | 2 + ...supervisor_federationdomain_status_test.go | 192 ++++++++++++------ 2 files changed, 131 insertions(+), 63 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 5f2c9604d..9c9321a3a 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1507,6 +1507,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin for the LDAP IDP. t.Log("starting second LDAP auth via kubectl") + timeoutCtx, cleanupTimeoutCtx = context.WithTimeout(testCtx, 10*time.Second) + t.Cleanup(cleanupTimeoutCtx) kubectlCmd = exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) ptyFile, err = pty.Start(kubectlCmd) diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index 333defe4a..4d57dbc12 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -6,6 +6,7 @@ package integration import ( "context" "fmt" + "strings" "testing" "time" @@ -13,11 +14,13 @@ import ( corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" "k8s.io/utils/pointer" "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/testutil" "go.pinniped.dev/test/testlib" ) @@ -188,6 +191,8 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { name: "spec with explicit identity providers and lots of validation errors", run: func(t *testing.T) { + federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) + oidcIdentityProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: "https://example.cluster.local/fake-issuer-url-does-not-matter", Client: idpv1alpha1.OIDCClient{SecretName: "this-will-not-exist-but-does-not-matter"}, @@ -334,6 +339,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { }, }, }, v1alpha1.FederationDomainPhaseError) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), []v1alpha1.Condition{ @@ -350,8 +356,8 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { )}, { Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "False", Reason: "APIGroupUnrecognized", - Message: `some API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized ` + - `(should be "idp.supervisor.pinniped.dev"): "this is the wrong api group"`, + Message: fmt.Sprintf(`some API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized `+ + `(should be "idp.supervisor.%s"): "this is the wrong api group"`, env.APIGroupSuffix), }, { Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized", @@ -427,50 +433,56 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { )) // Updating the FederationDomain to fix some of the problems should make some of the errors go away. - federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace) - fd, err := federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) - require.NoError(t, err) - fd.Spec.IdentityProviders[0] = v1alpha1.FederationDomainIdentityProvider{ - // Fix the display name. - DisplayName: "now made unique", - // Fix the objectRef. - ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), - Kind: "OIDCIdentityProvider", - Name: oidcIdentityProvider.Name, - }, - Transforms: v1alpha1.FederationDomainTransforms{ - Constants: []v1alpha1.FederationDomainTransformsConstant{ - {Name: "foo", Type: "string", StringValue: "bar"}, - }, - Expressions: []v1alpha1.FederationDomainTransformsExpression{ - // Fix the compile errors. - {Type: "username/v1", Expression: `"pre:" + username`}, - }, - Examples: []v1alpha1.FederationDomainTransformsExample{ - { // this example should fail because it expects both the wrong username and groups - Username: "ryan", - Groups: []string{"b", "a"}, - Expects: v1alpha1.FederationDomainTransformsExampleExpects{ - Username: "wrong", - Groups: []string{}, - Rejected: false, + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + gotFD, err := federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) + require.NoError(t, err) + + gotFD.Spec.IdentityProviders[0] = v1alpha1.FederationDomainIdentityProvider{ + // Fix the display name. + DisplayName: "now made unique", + // Fix the objectRef. + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + }, + Transforms: v1alpha1.FederationDomainTransforms{ + Constants: []v1alpha1.FederationDomainTransformsConstant{ + {Name: "foo", Type: "string", StringValue: "bar"}, + }, + Expressions: []v1alpha1.FederationDomainTransformsExpression{ + // Fix the compile errors. + {Type: "username/v1", Expression: `"pre:" + username`}, + }, + Examples: []v1alpha1.FederationDomainTransformsExample{ + { // this example should fail because it expects both the wrong username and groups + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "wrong", + Groups: []string{}, + Rejected: false, + }, }, }, }, - }, - } - fd.Spec.IdentityProviders[2].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ - { // this example should pass - Username: "other", - Expects: v1alpha1.FederationDomainTransformsExampleExpects{ - Rejected: true, - Message: "only special users allowed", + } + + gotFD.Spec.IdentityProviders[2].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "other", + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Rejected: true, + Message: "only special users allowed", + }, }, - }, - } - fd, err = federationDomainsClient.Update(ctx, fd, metav1.UpdateOptions{}) + } + + _, updateErr := federationDomainsClient.Update(ctx, gotFD, metav1.UpdateOptions{}) + return updateErr + }) require.NoError(t, err) + testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), []v1alpha1.Condition{ @@ -503,25 +515,32 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { )) // Updating the FederationDomain to fix the rest of the problems should make all the errors go away. - fd, err = federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) - require.NoError(t, err) - fd.Spec.IdentityProviders[2].ObjectRef = corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), - Kind: "OIDCIdentityProvider", - Name: oidcIdentityProvider.Name, - } - fd.Spec.IdentityProviders[0].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ - { // this example should pass - Username: "ryan", - Groups: []string{"b", "a"}, - Expects: v1alpha1.FederationDomainTransformsExampleExpects{ - Username: "pre:ryan", - Groups: []string{"a", "b"}, + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + gotFD, err := federationDomainsClient.Get(ctx, fd.Name, metav1.GetOptions{}) + require.NoError(t, err) + + gotFD.Spec.IdentityProviders[2].ObjectRef = corev1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIdentityProvider.Name, + } + + gotFD.Spec.IdentityProviders[0].Transforms.Examples = []v1alpha1.FederationDomainTransformsExample{ + { // this example should pass + Username: "ryan", + Groups: []string{"b", "a"}, + Expects: v1alpha1.FederationDomainTransformsExampleExpects{ + Username: "pre:ryan", + Groups: []string{"a", "b"}, + }, }, - }, - } - fd, err = federationDomainsClient.Update(ctx, fd, metav1.UpdateOptions{}) + } + + _, updateErr := federationDomainsClient.Update(ctx, gotFD, metav1.UpdateOptions{}) + return updateErr + }) require.NoError(t, err) + testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseReady) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, allSuccessfulFederationDomainConditions(fd.Spec)) }, @@ -543,12 +562,18 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) t.Cleanup(cancel) + adminClient := testlib.NewKubernetesClientset(t) + usingOldKubeVersionInCluster := testutil.KubeServerMinorVersionInBetweenInclusive(t, adminClient.Discovery(), 0, 23) + usingReallyOldKubeVersionInCluster := testutil.KubeServerMinorVersionInBetweenInclusive(t, adminClient.Discovery(), 0, 19) + objectMeta := testlib.ObjectMetaWithRandomName(t, "federation-domain") tests := []struct { - name string - fd *v1alpha1.FederationDomain - wantErr string + name string + fd *v1alpha1.FederationDomain + wantErr string + wantOldKubeErr string + wantReallyOldKubeErr string }{ { name: "issuer cannot be empty", @@ -605,6 +630,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { }, }, }, + wantOldKubeErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders[0].transforms.constants[1]: Duplicate value: map[string]interface {}{"name":"notUnique"}`, + env.APIGroupSuffix, objectMeta.Name), wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ `spec.identityProviders[0].transforms.constants[1]: Duplicate value: map[string]interface {}{"name":"notUnique"}`, env.APIGroupSuffix, objectMeta.Name), @@ -656,6 +684,14 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { }, }, }, + wantReallyOldKubeErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders.transforms.constants.name: Invalid value: "": `+ + `spec.identityProviders.transforms.constants.name in body should be at most 64 chars long`, + env.APIGroupSuffix, objectMeta.Name), + wantOldKubeErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders.transforms.constants.name: Invalid value: "12345678901234567890123456789012345678901234567890123456789012345": `+ + `spec.identityProviders.transforms.constants.name in body should be at most 64 chars long`, + env.APIGroupSuffix, objectMeta.Name), wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ `spec.identityProviders[0].transforms.constants[0].name: Too long: may not be longer than 64`, env.APIGroupSuffix, objectMeta.Name), @@ -685,6 +721,14 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { }, }, }, + wantReallyOldKubeErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders.transforms.constants.name: Invalid value: "": `+ + `spec.identityProviders.transforms.constants.name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$'`, + env.APIGroupSuffix, objectMeta.Name), + wantOldKubeErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ + `spec.identityProviders.transforms.constants.name: Invalid value: "cannot have spaces": `+ + `spec.identityProviders.transforms.constants.name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$'`, + env.APIGroupSuffix, objectMeta.Name), wantErr: fmt.Sprintf("FederationDomain.config.supervisor.%s %q is invalid: "+ `[spec.identityProviders[0].transforms.constants[0].name: Invalid value: "cannot have spaces": `+ `spec.identityProviders[0].transforms.constants[0].name in body should match '^[a-zA-Z][_a-zA-Z0-9]*$', `+ @@ -875,10 +919,32 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { } }) - if tt.wantErr == "" { + if tt.wantErr == "" && tt.wantOldKubeErr == "" && tt.wantReallyOldKubeErr == "" { require.NoError(t, createErr) } else { - require.EqualError(t, createErr, tt.wantErr) + wantErr := tt.wantErr + if usingOldKubeVersionInCluster || usingReallyOldKubeVersionInCluster { + // Old versions of Kubernetes did not show the index where the error occurred in some of the messages, + // so remove the indices from the expected messages when running against an old version of Kube. + // For the above tests, it should be enough to assume that there will only be indices up to 10. + // This is useful when the only difference in the message between old and new is the missing indices. + // Otherwise, use wantOldKubeErr to say what the expected message should be for old versions. + for i := 0; i < 10; i++ { + wantErr = strings.ReplaceAll(wantErr, fmt.Sprintf("[%d]", i), "") + } + } + if usingOldKubeVersionInCluster && tt.wantOldKubeErr != "" { + // Sometimes there are other difference in older Kubernetes messages, so also allow exact + // expectation strings for those cases in wantOldKubeErr. When provided, use it on old Kube clusters. + wantErr = tt.wantOldKubeErr + } + if usingReallyOldKubeVersionInCluster && tt.wantReallyOldKubeErr != "" { + // Sometimes there are other difference in really old Kubernetes messages, so also allow exact + // expectation strings for those cases in wantOldKubeErr. When provided, use it on + // really old Kube clusters. + wantErr = tt.wantReallyOldKubeErr + } + require.EqualError(t, createErr, wantErr) } }) } From 519aece8a5585ade986fcad86c3d169d1392d6d3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 28 Jul 2023 09:47:24 -0700 Subject: [PATCH 64/81] Start adding identity transformations tests to supervisor_login_test.go --- test/integration/supervisor_login_test.go | 194 +++++++++++++++++++--- 1 file changed, 175 insertions(+), 19 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index dcaf45d9f..700b32d0b 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -26,6 +26,7 @@ import ( "golang.org/x/oauth2" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" "k8s.io/utils/strings/slices" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -60,6 +61,14 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) } + addPrefixToEach := func(prefix string, addToEach []string) []string { + result := make([]string, len(addToEach)) + for i, s := range addToEach { + result[i] = fmt.Sprintf("%s%s", prefix, s) + } + return result + } + basicOIDCIdentityProviderSpec := func() idpv1alpha1.OIDCIdentityProviderSpec { return idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -166,8 +175,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { // OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go. // // Each of these tests perform the following flow: - // 1. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available. - // 2. Configure an IDP CR. + // 1. Configure an IDP CR. + // 2. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available. // 3. Call the authorization endpoint and log in as a specific user. // Note that these tests do not use form_post response type (which is tested by e2e_test.go). // 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure. @@ -190,6 +199,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // its cleanup. Return the name of the IDP CR. createIDP func(t *testing.T) string + // Optionally specify the identityProviders part of the FederationDomain's spec by returning it from this function. + // Also return the displayName of the IDP that should be used during authentication. + // This function takes the name of the IDP CR which was returned by createIDP() as as argument. + federationDomainIDPs func(t *testing.T, idpName string) (idps []configv1alpha1.FederationDomainIdentityProvider, useIDPDisplayName string) + // Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the // test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret. // When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693 @@ -297,8 +311,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, 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 OIDC server, - // we expect that it will return to the real groups from the OIDC server after we refresh. + // Even if we update the groups to some names that did not come from the OIDC server, + // we expect that it will revert to the real groups from the OIDC server after we refresh. // However if there are no expected groups then they will not update, so we should skip this. if len(env.SupervisorUpstreamOIDC.ExpectedGroups) > 0 { initialGroupMembership := []string{"some-wrong-group", "some-other-group"} @@ -1836,6 +1850,138 @@ func TestSupervisorLogin_Browser(t *testing.T) { requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, wantAuthcodeExchangeError: `oauth2: "invalid_client" "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."`, }, + { + name: "oidc with custom username and groups claim settings with identity transformations", + maybeSkip: skipNever, + createIDP: func(t *testing.T) string { + spec := basicOIDCIdentityProviderSpec() + spec.Claims = idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + } + spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + } + return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "my oidc idp" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: v1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: idpName, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "username/v1", + Expression: fmt.Sprintf(`username == "%s" ? "username-prefix:" + username : username`, + env.SupervisorUpstreamOIDC.Username), + }, + { + Type: "groups/v1", + Expression: `groups.map(g, "group-prefix:" + g)`, + }, + }, + }, + }, + }, displayName + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamOIDC.Username) + "$" + }, + wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups), + editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { + // Even if we update the groups to some names that did not come from the OIDC server, + // we expect that it will revert to the real groups from the OIDC server after we refresh. + // However if there are no expected groups then they will not update, so we should skip this. + if len(env.SupervisorUpstreamOIDC.ExpectedGroups) > 0 { + 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + // Simulate what happens when the transformations choose a different downstream username during refresh + // compared to what they chose during the initial login, by changing what they decided during the initial login. + customSessionData.Username = "some-different-downstream-username" + }, + }, + { + name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS with identity transformations", + maybeSkip: skipLDAPTests, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "my ldap idp" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: v1.TypedLocalObjectReference{ + APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + Kind: "LDAPIdentityProvider", + Name: idpName, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "username/v1", + Expression: fmt.Sprintf(`username == "%s" ? "username-prefix:" + username : username`, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), + }, + { + Type: "groups/v1", + Expression: `groups.map(g, "group-prefix:" + g)`, + }, + }, + }, + }, + }, displayName + }, + 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + // Simulate what happens when the transformations choose a different downstream username during refresh + // compared to what they chose during the initial login, by changing what they decided during the initial login. + customSessionData.Username = "some-different-downstream-username" + }, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), + }, } for _, test := range tests { @@ -1846,6 +1992,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { testSupervisorLogin( t, tt.createIDP, + tt.federationDomainIDPs, tt.requestAuthorization, tt.editRefreshSessionDataWithoutBreaking, tt.breakRefreshSessionData, @@ -2000,6 +2147,7 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T) string, + federationDomainIDPs func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string), requestAuthorization func(t *testing.T, downstreamIssuer string, downstreamAuthorizeURL string, downstreamCallbackURL string, username string, password string, httpClient *http.Client), editRefreshSessionDataWithoutBreaking func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string, username string) []string, breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string, username string), @@ -2076,23 +2224,28 @@ func testSupervisorLogin( map[string]string{"tls.crt": string(certPEM), "tls.key": string(keyPEM)}, ) - // Create the downstream FederationDomain and expect it to go into the success status condition. + // Create upstream IDP and wait for it to become ready. + idpName := createIDP(t) + + // Determine if and how we should set spec.identityProviders for the FederationDomain. + var fdIDPSpec []configv1alpha1.FederationDomainIdentityProvider + useIDPDisplayName := "" + if federationDomainIDPs != nil { + fdIDPSpec, useIDPDisplayName = federationDomainIDPs(t, idpName) + } + + // Create the downstream FederationDomain and expect it to go into the appropriate status condition. downstream := testlib.CreateTestFederationDomain(ctx, t, configv1alpha1.FederationDomainSpec{ - Issuer: issuerURL.String(), - TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + Issuer: issuerURL.String(), + TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, + IdentityProviders: fdIDPSpec, }, - // This is a legacy FederationDomain (does not explicitly list any IDPs) and there is no IDP yet, - // so it should not be ready yet. - configv1alpha1.FederationDomainPhaseError, + // The IDP CR already exists, so even for legacy FederationDomains which do not explicitly list + // the IDPs in the spec, the FederationDomain should be ready. + configv1alpha1.FederationDomainPhaseReady, ) - // Create upstream IDP and wait for it to become ready. - idpName := createIDP(t) - - // Now that both the FederationDomain and the IDP are created, the FederationDomain should be ready. - testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady) - // Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for // the `/jwks.json` endpoint to succeed, because there is no point in proceeding and eventually // calling the token endpoint from this test until the JWKS data has been loaded into @@ -2171,12 +2324,15 @@ func testSupervisorLogin( require.NoError(t, err) pkceParam, err := pkce.Generate() require.NoError(t, err) - downstreamAuthorizeURL := downstreamOAuth2Config.AuthCodeURL( - stateParam.String(), + authorizeRequestParams := []oauth2.AuthCodeOption{ nonceParam.Param(), pkceParam.Challenge(), pkceParam.Method(), - ) + } + if useIDPDisplayName != "" { + authorizeRequestParams = append(authorizeRequestParams, oauth2.SetAuthURLParam("pinniped_idp_name", useIDPDisplayName)) + } + downstreamAuthorizeURL := downstreamOAuth2Config.AuthCodeURL(stateParam.String(), authorizeRequestParams...) // Perform parameterized auth code acquisition. requestAuthorization(t, downstream.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) From 0a21cb6d0840ab4451bc19a0a7852e3ac3fd70a5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 28 Jul 2023 12:46:15 -0700 Subject: [PATCH 65/81] Replace more pointer.String() with the new ptr.To() --- .../federation_domain_watcher_test.go | 60 +++++++++---------- test/integration/e2e_test.go | 6 +- ...supervisor_federationdomain_status_test.go | 38 ++++++------ test/integration/supervisor_login_test.go | 6 +- 4 files changed, 55 insertions(+), 55 deletions(-) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index c3bbf433c..38c4cc54f 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -20,7 +20,7 @@ import ( "k8s.io/apimachinery/pkg/types" coretesting "k8s.io/client-go/testing" clocktesting "k8s.io/utils/clock/testing" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -993,7 +993,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "cant-find-me", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: "cant-find-me-name", }, @@ -1001,7 +1001,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "cant-find-me-either", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: "cant-find-me-either-name", }, @@ -1009,7 +1009,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "cant-find-me-still", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: "cant-find-me-still-name", }, @@ -1054,7 +1054,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1062,7 +1062,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "can-find-me-too", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1070,7 +1070,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "can-find-me-three", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, @@ -1123,7 +1123,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1131,7 +1131,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1139,7 +1139,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "duplicate1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1147,7 +1147,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, @@ -1155,7 +1155,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "duplicate2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1163,7 +1163,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "duplicate2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, @@ -1202,7 +1202,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("wrong.example.com"), + APIGroup: ptr.To("wrong.example.com"), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1210,7 +1210,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(""), // empty string is wrong + APIGroup: ptr.To(""), // empty string is wrong Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1226,7 +1226,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name4", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), // correct + APIGroup: ptr.To(apiGroupSupervisor), // correct Kind: "ActiveDirectoryIdentityProvider", Name: adIdentityProvider.Name, }, @@ -1272,7 +1272,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", // correct Name: oidcIdentityProvider.Name, }, @@ -1280,7 +1280,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "wrong", Name: ldapIdentityProvider.Name, }, @@ -1288,7 +1288,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name3", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "", // empty is also wrong Name: ldapIdentityProvider.Name, }, @@ -1330,7 +1330,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1393,7 +1393,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1557,7 +1557,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1628,7 +1628,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "not unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: "this will not be found", }, @@ -1663,7 +1663,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "not unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "this is wrong", Name: "foo", }, @@ -1698,7 +1698,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("this is wrong"), + APIGroup: ptr.To("this is wrong"), Kind: "OIDCIdentityProvider", Name: "foo", }, @@ -1722,7 +1722,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1842,7 +1842,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1894,7 +1894,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "name2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "LDAPIdentityProvider", Name: ldapIdentityProvider.Name, }, @@ -1968,7 +1968,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -1998,7 +1998,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { { DisplayName: "can-find-me", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String(apiGroupSupervisor), + APIGroup: ptr.To(apiGroupSupervisor), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 9c9321a3a..48074af3b 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -31,7 +31,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" authv1alpha "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -1283,7 +1283,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { { DisplayName: ldapIDPDisplayName, ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "LDAPIdentityProvider", Name: createdLDAPProvider.Name, }, @@ -1320,7 +1320,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { { DisplayName: oidcIDPDisplayName, ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: createdOIDCProvider.Name, }, diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index 4d57dbc12..d28718a50 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -15,7 +15,7 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -104,7 +104,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { DisplayName: "idp1", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: oidcIDP1Meta.Name, }, @@ -113,7 +113,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { DisplayName: "idp2", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: oidcIDP2Meta.Name, }, @@ -204,7 +204,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { DisplayName: "not unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("this is the wrong api group"), + APIGroup: ptr.To("this is the wrong api group"), Kind: "OIDCIdentityProvider", Name: "will not be found", }, @@ -228,7 +228,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { // this identity provider should be valid DisplayName: "unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -236,7 +236,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { DisplayName: "not unique", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "this is the wrong kind", Name: "also will not be found", }, @@ -442,7 +442,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { DisplayName: "now made unique", // Fix the objectRef. ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, }, @@ -520,7 +520,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { require.NoError(t, err) gotFD.Spec.IdentityProviders[2].ObjectRef = corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: oidcIdentityProvider.Name, } @@ -597,7 +597,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, }, }, @@ -618,7 +618,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ @@ -647,7 +647,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ @@ -673,7 +673,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ @@ -706,7 +706,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ @@ -748,7 +748,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ @@ -776,7 +776,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Expressions: []v1alpha1.FederationDomainTransformsExpression{ @@ -805,7 +805,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Expressions: []v1alpha1.FederationDomainTransformsExpression{ @@ -831,7 +831,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Examples: []v1alpha1.FederationDomainTransformsExample{ @@ -867,7 +867,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, }, }, @@ -884,7 +884,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { { DisplayName: "foo", ObjectRef: corev1.TypedLocalObjectReference{ - APIGroup: pointer.String("required in older versions of Kubernetes for each item in the identityProviders slice"), + APIGroup: ptr.To("required in older versions of Kubernetes for each item in the identityProviders slice"), }, Transforms: v1alpha1.FederationDomainTransforms{ Constants: []v1alpha1.FederationDomainTransformsConstant{ diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 700b32d0b..2395e6a3a 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -26,7 +26,7 @@ import ( "golang.org/x/oauth2" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" "k8s.io/utils/strings/slices" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -1870,7 +1870,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { { DisplayName: displayName, ObjectRef: v1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "OIDCIdentityProvider", Name: idpName, }, @@ -1927,7 +1927,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { { DisplayName: displayName, ObjectRef: v1.TypedLocalObjectReference{ - APIGroup: pointer.String("idp.supervisor." + env.APIGroupSuffix), + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), Kind: "LDAPIdentityProvider", Name: idpName, }, From 2eb82cc1d7f63a33f4e0f022989f7dd5a391a09e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 28 Jul 2023 14:29:20 -0700 Subject: [PATCH 66/81] Add more tests with identity transformations in supervisor_login_test.go --- test/integration/supervisor_login_test.go | 220 ++++++++++++++++++++-- 1 file changed, 209 insertions(+), 11 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 2395e6a3a..20205219b 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -1805,16 +1805,12 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, }, configv1alpha1.OIDCClientPhaseReady) }, - requestAuthorization: func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { - requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t, - downstreamIssuer, - downstreamAuthorizeURL, - downstreamCallbackURL, - env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login - env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login - httpClient, - ) + testUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword // password to present to server during login }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ @@ -1851,7 +1847,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantAuthcodeExchangeError: `oauth2: "invalid_client" "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."`, }, { - name: "oidc with custom username and groups claim settings with identity transformations", + name: "oidc browser flow with custom username and groups claim settings with identity transformations", maybeSkip: skipNever, createIDP: func(t *testing.T) string { spec := basicOIDCIdentityProviderSpec() @@ -1915,7 +1911,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, }, { - name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS with identity transformations", + name: "ldap CLI flow with email as username and groups names as DNs and using an LDAP provider which supports TLS with identity transformations", maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { idp, _ := createLDAPIdentityProvider(t, nil) @@ -1982,6 +1978,208 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), }, + { + name: "ldap browser flow with email as username and groups names as DNs and using an LDAP provider which supports TLS with identity transformations", + maybeSkip: skipLDAPTests, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "my ldap idp" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: v1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "LDAPIdentityProvider", + Name: idpName, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "username/v1", + Expression: fmt.Sprintf(`username == "%s" ? "username-prefix:" + username : username`, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), + }, + { + Type: "groups/v1", + Expression: `groups.map(g, "group-prefix:" + g)`, + }, + }, + }, + }, + }, displayName + }, + testUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + customSessionData := pinnipedSession.Custom + // Simulate what happens when the transformations choose a different downstream username during refresh + // compared to what they chose during the initial login, by changing what they decided during the initial login. + customSessionData.Username = "some-different-downstream-username" + }, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), + }, + { + name: "active directory CLI flow with all default options with identity transformations", + maybeSkip: skipActiveDirectoryTests, + createIDP: func(t *testing.T) string { + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "my ad idp" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: v1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "ActiveDirectoryIdentityProvider", + Name: idpName, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "username/v1", + Expression: fmt.Sprintf(`username == "%s" ? "username-prefix:" + username : username`, + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue), + }, + { + Type: "groups/v1", + Expression: `groups.map(g, "group-prefix:" + g)`, + }, + }, + }, + }, + }, displayName + }, + 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, + ) + }, + 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 AD server, + // we expect that it will return to the real groups from the AD 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) + }, + 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) + // Simulate what happens when the transformations choose a different downstream username during refresh + // compared to what they chose during the initial login, by changing what they decided during the initial login. + customSessionData.Username = "some-different-downstream-username" + }, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ + "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" + }, + wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), + }, + { + name: "active directory browser flow with all default options with identity transformations", + maybeSkip: skipActiveDirectoryTests, + createIDP: func(t *testing.T) string { + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "my ad idp" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: v1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "ActiveDirectoryIdentityProvider", + Name: idpName, + }, + Transforms: configv1alpha1.FederationDomainTransforms{ + Expressions: []configv1alpha1.FederationDomainTransformsExpression{ + { + Type: "username/v1", + Expression: fmt.Sprintf(`username == "%s" ? "username-prefix:" + username : username`, + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue), + }, + { + Type: "groups/v1", + Expression: `groups.map(g, "group-prefix:" + g)`, + }, + }, + }, + }, + }, displayName + }, + testUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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 AD server, + // we expect that it will return to the real groups from the AD 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) + }, + 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) + // Simulate what happens when the transformations choose a different downstream username during refresh + // compared to what they chose during the initial login, by changing what they decided during the initial login. + customSessionData.Username = "some-different-downstream-username" + }, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ + "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" + }, + wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), + }, } for _, test := range tests { From b2656b9cb188f11bd17a8ae836a63983d9e30a7f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 22 Aug 2023 17:28:56 -0700 Subject: [PATCH 67/81] add new unit tests in auth_handler_test.go --- .../endpoints/auth/auth_handler_test.go | 368 ++++++++++++++---- .../testutil/oidctestutil/oidctestutil.go | 11 +- internal/testutil/string_slice.go | 14 + test/integration/supervisor_login_test.go | 28 +- 4 files changed, 317 insertions(+), 104 deletions(-) create mode 100644 internal/testutil/string_slice.go diff --git a/internal/federationdomain/endpoints/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go index cb4750875..ba83e7efe 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -30,12 +30,14 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/celtransformer" "go.pinniped.dev/internal/federationdomain/csrftoken" "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -43,7 +45,7 @@ import ( "go.pinniped.dev/pkg/oidcclient/pkce" ) -func TestAuthorizationEndpoint(t *testing.T) { +func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo const ( oidcUpstreamName = "some-oidc-idp" oidcUpstreamResourceUID = "oidc-resource-uid" @@ -80,6 +82,9 @@ func TestAuthorizationEndpoint(t *testing.T) { pinnipedCLIClientID = "pinniped-cli" dynamicClientID = "client.oauth.pinniped.dev-test-name" dynamicClientUID = "fake-client-uid" + + transformationUsernamePrefix = "username_prefix:" + transformationGroupsPrefix = "groups_prefix:" ) require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case") @@ -221,6 +226,12 @@ func TestAuthorizationEndpoint(t *testing.T) { "state": happyState, } + fositeAccessDeniedWithConfiguredPolicyRejectionHintErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Reason: configured identity policy rejected this authentication: authentication was rejected by a configured policy.", + "state": happyState, + } + fositeLoginRequiredErrorQuery = map[string]string{ "error": "login_required", "error_description": "The Authorization Server requires End-User authentication.", @@ -324,19 +335,21 @@ func TestAuthorizationEndpoint(t *testing.T) { return nil, false, nil } - upstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). - WithName(ldapUpstreamName). - WithResourceUID(ldapUpstreamResourceUID). - WithURL(parsedUpstreamLDAPURL). - WithAuthenticateFunc(ldapAuthenticateFunc). - Build() + upstreamLDAPIdentityProviderBuilder := func() *oidctestutil.TestUpstreamLDAPIdentityProviderBuilder { + return oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc) + } - upstreamActiveDirectoryIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). - WithName(activeDirectoryUpstreamName). - WithResourceUID(activeDirectoryUpstreamResourceUID). - WithURL(parsedUpstreamLDAPURL). - WithAuthenticateFunc(ldapAuthenticateFunc). - Build() + upstreamActiveDirectoryIdentityProviderBuilder := func() *oidctestutil.TestUpstreamLDAPIdentityProviderBuilder { + return oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(activeDirectoryUpstreamName). + WithResourceUID(activeDirectoryUpstreamResourceUID). + WithURL(parsedUpstreamLDAPURL). + WithAuthenticateFunc(ldapAuthenticateFunc) + } erroringUpstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). @@ -550,16 +563,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, } - expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups := func(wantUsername string, wantGroups []string) *psession.CustomSessionData { - copyOfCustomSession := *expectedHappyOIDCPasswordGrantCustomSession - copyOfOIDC := *(expectedHappyOIDCPasswordGrantCustomSession.OIDC) - copyOfCustomSession.OIDC = ©OfOIDC - copyOfCustomSession.Username = wantUsername - copyOfCustomSession.UpstreamUsername = wantUsername - copyOfCustomSession.UpstreamGroups = wantGroups - return ©OfCustomSession - } - expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken := &psession.CustomSessionData{ Username: oidcUpstreamUsername, UpstreamUsername: oidcUpstreamUsername, @@ -574,6 +577,26 @@ func TestAuthorizationEndpoint(t *testing.T) { }, } + withUsernameAndGroupsInCustomSession := func(expectedCustomSessionData *psession.CustomSessionData, wantDownstreamUsername string, wantUpstreamUsername string, wantUpstreamGroups []string) *psession.CustomSessionData { + copyOfCustomSession := *expectedCustomSessionData + if expectedCustomSessionData.LDAP != nil { + copyOfLDAP := *(expectedCustomSessionData.LDAP) + copyOfCustomSession.LDAP = ©OfLDAP + } + if expectedCustomSessionData.OIDC != nil { + copyOfOIDC := *(expectedCustomSessionData.OIDC) + copyOfCustomSession.OIDC = ©OfOIDC + } + if expectedCustomSessionData.ActiveDirectory != nil { + copyOfActiveDirectory := *(expectedCustomSessionData.ActiveDirectory) + copyOfCustomSession.ActiveDirectory = ©OfActiveDirectory + } + copyOfCustomSession.Username = wantDownstreamUsername + copyOfCustomSession.UpstreamUsername = wantUpstreamUsername + copyOfCustomSession.UpstreamGroups = wantUpstreamGroups + return ©OfCustomSession + } + addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t, "some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, @@ -589,6 +612,24 @@ func TestAuthorizationEndpoint(t *testing.T) { encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue) require.NoError(t, err) + transformer, err := celtransformer.NewCELTransformer(5 * time.Second) // CI workers can be slow, so allow slow transforms + require.NoError(t, err) + + prefixUsernameAndGroupsPipeline := idtransform.NewTransformationPipeline() + rejectAuthPipeline := idtransform.NewTransformationPipeline() + + var compiledTransform idtransform.IdentityTransformation + compiledTransform, err = transformer.CompileTransformation(&celtransformer.UsernameTransformation{Expression: fmt.Sprintf(`"%s" + username`, transformationUsernamePrefix)}, nil) + require.NoError(t, err) + prefixUsernameAndGroupsPipeline.AppendTransformation(compiledTransform) + compiledTransform, err = transformer.CompileTransformation(&celtransformer.GroupsTransformation{Expression: fmt.Sprintf(`groups.map(g, "%s" + g)`, transformationGroupsPrefix)}, nil) + require.NoError(t, err) + prefixUsernameAndGroupsPipeline.AppendTransformation(compiledTransform) + + compiledTransform, err = transformer.CompileTransformation(&celtransformer.AllowAuthenticationPolicy{Expression: `username == "someone-special"`}, nil) + require.NoError(t, err) + rejectAuthPipeline.AppendTransformation(compiledTransform) + type testCase struct { name string @@ -673,7 +714,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -688,9 +729,65 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "OIDC upstream browser flow happy path using GET without a CSRF cookie using backwards compatibility mode to have a default IDP (display name does not need to be sent as query param)", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). + WithDefaultIDPDisplayName(oidcUpstreamName), // specify which IDP is the backwards-compatibility mode IDP + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPath, // does not include IDP display name as query param + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, + { + name: "with multiple IDPs available, request chooses to use OIDC browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). + WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPathForOIDCUpstream, // includes IDP display name of OIDC upstream + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, + { + name: "with multiple IDPs available, request chooses to use LDAP browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). + WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPathForLDAPUpstream, // includes IDP display name of LDAP upstream + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -708,7 +805,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -725,7 +822,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -763,6 +860,48 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, + { + name: "OIDC upstream password grant happy path using GET with identity transformations which change username and groups", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + method: http.MethodGet, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, + customUsernameHeader: ptr.To(oidcUpstreamUsername), + customPasswordHeader: ptr.To(oidcUpstreamPassword), + wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + oidcUpstreamUsername, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, oidcUpstreamGroupMembership), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + transformationUsernamePrefix+oidcUpstreamUsername, + oidcUpstreamUsername, + oidcUpstreamGroupMembership, + ), + }, + { + name: "OIDC upstream password grant with identity transformations which rejects auth", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + method: http.MethodGet, + path: happyGetRequestPathForOIDCPasswordGrantUpstream, + customUsernameHeader: ptr.To(oidcUpstreamUsername), + customPasswordHeader: ptr.To(oidcUpstreamPassword), + wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, + wantStatus: http.StatusFound, + wantContentType: jsonContentType, + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithConfiguredPolicyRejectionHintErrorQuery), + wantBodyString: "", + }, { name: "OIDC upstream password grant happy path using GET with additional claim mappings", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder(). @@ -827,7 +966,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -846,9 +985,49 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, + { + name: "LDAP cli upstream happy path using GET with identity transformations which change username and groups", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(upstreamLDAPIdentityProviderBuilder().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + method: http.MethodGet, + path: happyGetRequestPathForLDAPUpstream, + customUsernameHeader: ptr.To(happyLDAPUsername), + customPasswordHeader: ptr.To(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyLDAPUpstreamCustomSession, + transformationUsernamePrefix+happyLDAPUsernameFromAuthenticator, + happyLDAPUsernameFromAuthenticator, + happyLDAPGroups, + ), + }, + { + name: "LDAP cli upstream with identity transformations which reject auth", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(upstreamLDAPIdentityProviderBuilder().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + method: http.MethodGet, + path: happyGetRequestPathForLDAPUpstream, + customUsernameHeader: ptr.To(happyLDAPUsername), + customPasswordHeader: ptr.To(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: jsonContentType, + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithConfiguredPolicyRejectionHintErrorQuery), + wantBodyString: "", + }, { name: "ActiveDirectory cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -886,7 +1065,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -903,7 +1082,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -959,7 +1138,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -978,7 +1157,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -998,7 +1177,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1017,7 +1196,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1061,7 +1240,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1084,7 +1263,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "Active Directory cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1264,7 +1443,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client @@ -1430,7 +1609,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream password for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1442,7 +1621,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream password for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1454,7 +1633,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream username for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To("wrong-username"), @@ -1466,7 +1645,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream username for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To("wrong-username"), @@ -1490,7 +1669,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream username but has password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: nil, // do not send header @@ -1502,7 +1681,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream username on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: nil, // do not send header @@ -1514,7 +1693,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1526,7 +1705,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream password on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1653,7 +1832,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "dynamic clients are not allowed to use LDAP CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1666,7 +1845,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "dynamic clients are not allowed to use Active Directory CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1727,7 +1906,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1740,7 +1919,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1778,7 +1957,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1787,7 +1966,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1843,7 +2022,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1855,7 +2034,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -1865,7 +2044,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ @@ -1880,7 +2059,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1892,7 +2071,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -1902,7 +2081,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ @@ -1989,7 +2168,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2001,7 +2180,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client using Active Directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2056,7 +2235,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2068,7 +2247,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2078,7 +2257,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2089,7 +2268,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2101,7 +2280,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2111,7 +2290,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2147,7 +2326,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing client id in request using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": ""}), wantStatus: http.StatusUnauthorized, @@ -2201,7 +2380,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2259,7 +2438,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "invalid value for PKCE code_challenge_method in request using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2317,7 +2496,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "when PKCE code_challenge_method in request is `plain` using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": "plain"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2375,7 +2554,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge_method in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2441,7 +2620,7 @@ func TestAuthorizationEndpoint(t *testing.T) { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an LDAP upstream. name: "prompt param is not allowed to have none and another legal value at the same time using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"prompt": "none login"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2518,7 +2697,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"prompt": "none login", "scope": "email"}), @@ -2560,7 +2739,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, nil, ), @@ -2589,7 +2770,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + "joe@whitehouse.gov", "joe@whitehouse.gov", oidcUpstreamGroupMembership, ), @@ -2619,7 +2802,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + "joe@whitehouse.gov", "joe@whitehouse.gov", oidcUpstreamGroupMembership, ), @@ -2650,7 +2835,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + "joe", "joe", oidcUpstreamGroupMembership, ), @@ -2713,7 +2900,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + oidcUpstreamSubject, oidcUpstreamSubject, oidcUpstreamGroupMembership, ), @@ -2741,7 +2930,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + oidcUpstreamUsername, oidcUpstreamUsername, []string{"notAnArrayGroup1 notAnArrayGroup2"}, ), @@ -2769,7 +2960,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + oidcUpstreamUsername, oidcUpstreamUsername, []string{"group1", "group2"}, ), @@ -2811,7 +3004,9 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsernameAndGroups( + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyOIDCPasswordGrantCustomSession, + oidcUpstreamUsername, oidcUpstreamUsername, nil, ), @@ -3026,7 +3221,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream state does not have enough entropy using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"state": "short"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -3115,6 +3310,15 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: plainContentType, wantBodyString: `{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. 'pinniped_idp_name' param error: identity provider not found: this federation domain does not have a default identity provider"}`, }, + { + name: "could not find requested IDP display name", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: happyGetRequestPathForLDAPUpstream, // includes param to request a different IDP display name than what is available + wantStatus: http.StatusBadRequest, + wantContentType: plainContentType, + wantBodyString: `{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. 'pinniped_idp_name' param error: did not find IDP with name 'some-ldap-idp'"}`, + }, { name: "post with invalid form in the body", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 46aab19ac..bffe5ae73 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -467,10 +467,6 @@ type TestFederationDomainIdentityProvidersListerFinder struct { defaultIDPDisplayName string } -func (t *TestFederationDomainIdentityProvidersListerFinder) SetDefaultIDPDisplayName(displayName string) { - t.defaultIDPDisplayName = displayName -} - func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider { fdIDPs := make([]*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, len(t.upstreamOIDCIdentityProviders)) for i, testIDP := range t.upstreamOIDCIdentityProviders { @@ -570,6 +566,7 @@ type UpstreamIDPListerBuilder struct { upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider upstreamActiveDirectoryIdentityProviders []*TestUpstreamLDAPIdentityProvider + defaultIDPDisplayName string } func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { @@ -587,11 +584,17 @@ func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryId return b } +func (b *UpstreamIDPListerBuilder) WithDefaultIDPDisplayName(defaultIDPDisplayName string) *UpstreamIDPListerBuilder { + b.defaultIDPDisplayName = defaultIDPDisplayName + return b +} + func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerFinder() *TestFederationDomainIdentityProvidersListerFinder { return &TestFederationDomainIdentityProvidersListerFinder{ upstreamOIDCIdentityProviders: b.upstreamOIDCIdentityProviders, upstreamLDAPIdentityProviders: b.upstreamLDAPIdentityProviders, upstreamActiveDirectoryIdentityProviders: b.upstreamActiveDirectoryIdentityProviders, + defaultIDPDisplayName: b.defaultIDPDisplayName, } } diff --git a/internal/testutil/string_slice.go b/internal/testutil/string_slice.go new file mode 100644 index 000000000..d629698f1 --- /dev/null +++ b/internal/testutil/string_slice.go @@ -0,0 +1,14 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package testutil + +import "fmt" + +func AddPrefixToEach(prefix string, addToEach []string) []string { + result := make([]string, len(addToEach)) + for i, s := range addToEach { + result[i] = fmt.Sprintf("%s%s", prefix, s) + } + return result +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 20205219b..e7d25ecf4 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -61,14 +61,6 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) } - addPrefixToEach := func(prefix string, addToEach []string) []string { - result := make([]string, len(addToEach)) - for i, s := range addToEach { - result[i] = fmt.Sprintf("%s%s", prefix, s) - } - return result - } - basicOIDCIdentityProviderSpec := func() idpv1alpha1.OIDCIdentityProviderSpec { return idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -1891,7 +1883,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamOIDC.Username) + "$" }, - wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups), + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups), editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { // Even if we update the groups to some names that did not come from the OIDC server, // we expect that it will revert to the real groups from the OIDC server after we refresh. @@ -1901,7 +1893,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session } - return addPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups) + return testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamOIDC.ExpectedGroups) }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -1958,7 +1950,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) + return testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -1976,7 +1968,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" }, - wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), }, { name: "ldap browser flow with email as username and groups names as DNs and using an LDAP provider which supports TLS with identity transformations", @@ -2023,7 +2015,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) + return testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -2041,7 +2033,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" }, - wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs), }, { name: "active directory CLI flow with all default options with identity transformations", @@ -2091,7 +2083,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) + return testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -2111,7 +2103,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" }, - wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), }, { name: "active directory browser flow with all default options with identity transformations", @@ -2158,7 +2150,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { 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 addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) + return testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames) }, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { customSessionData := pinnipedSession.Custom @@ -2178,7 +2170,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" }, - wantDownstreamIDTokenGroups: addPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach("group-prefix:", env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames), }, } From d4611b829dc8d8ef02661b25ac2175b51a73be7a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 22 Aug 2023 17:29:44 -0700 Subject: [PATCH 68/81] use slices.Contains() instead of custom func in token_handler_test.go --- .../endpoints/token/token_handler_test.go | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 764f2c289..1ce813111 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -38,6 +38,7 @@ import ( "k8s.io/apiserver/pkg/warning" "k8s.io/client-go/kubernetes/fake" v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/utils/strings/slices" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" @@ -4118,7 +4119,7 @@ func TestRefreshGrant(t *testing.T) { ) if test.refreshRequest.want.wantStatus == http.StatusOK { - wantIDToken := contains(test.refreshRequest.want.wantSuccessBodyFields, "id_token") + wantIDToken := slices.Contains(test.refreshRequest.want.wantSuccessBodyFields, "id_token") var parsedRefreshResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(refreshResponse.Body.Bytes(), &parsedRefreshResponseBody)) @@ -4283,8 +4284,8 @@ func requireTokenEndpointBehavior( require.NoError(t, json.Unmarshal(tokenEndpointResponse.Body.Bytes(), &parsedResponseBody)) require.ElementsMatch(t, test.wantSuccessBodyFields, getMapKeys(parsedResponseBody)) - wantIDToken := contains(test.wantSuccessBodyFields, "id_token") - wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token") + wantIDToken := slices.Contains(test.wantSuccessBodyFields, "id_token") + wantRefreshToken := slices.Contains(test.wantSuccessBodyFields, "refresh_token") requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets, requestTime) requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime) @@ -4685,7 +4686,7 @@ func requireValidOIDCStorage( ) { t.Helper() - if contains(wantGrantedScopes, "openid") { + if slices.Contains(wantGrantedScopes, "openid") { // Make sure the OIDC session is still there. Note that Fosite stores OIDC sessions using the full auth code as a key. storedRequest, err := storage.GetOpenIDConnectSession(context.Background(), code, nil) require.NoError(t, err) @@ -4951,15 +4952,6 @@ func getMapKeys(m map[string]interface{}) []string { return keys } -func contains(haystack []string, needle string) bool { - for _, hay := range haystack { - if hay == needle { - return true - } - } - return false -} - func toSliceOfInterface(s []string) []interface{} { r := make([]interface{}, len(s)) for i := range s { From f653942065f47c48401e65d01b173081780a16fc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 23 Aug 2023 10:16:04 -0700 Subject: [PATCH 69/81] add new unit tests in callback_handler_test.go --- .../endpoints/auth/auth_handler_test.go | 22 +--- .../callback/callback_handler_test.go | 110 +++++++++++++++--- .../transformtestutil/transformtestutil.go | 58 +++++++++ 3 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 internal/testutil/transformtestutil/transformtestutil.go diff --git a/internal/federationdomain/endpoints/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go index ba83e7efe..48fced93b 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -30,17 +30,16 @@ import ( supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1" "go.pinniped.dev/internal/authenticators" - "go.pinniped.dev/internal/celtransformer" "go.pinniped.dev/internal/federationdomain/csrftoken" "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/federationdomain/storage" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -612,23 +611,8 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue) require.NoError(t, err) - transformer, err := celtransformer.NewCELTransformer(5 * time.Second) // CI workers can be slow, so allow slow transforms - require.NoError(t, err) - - prefixUsernameAndGroupsPipeline := idtransform.NewTransformationPipeline() - rejectAuthPipeline := idtransform.NewTransformationPipeline() - - var compiledTransform idtransform.IdentityTransformation - compiledTransform, err = transformer.CompileTransformation(&celtransformer.UsernameTransformation{Expression: fmt.Sprintf(`"%s" + username`, transformationUsernamePrefix)}, nil) - require.NoError(t, err) - prefixUsernameAndGroupsPipeline.AppendTransformation(compiledTransform) - compiledTransform, err = transformer.CompileTransformation(&celtransformer.GroupsTransformation{Expression: fmt.Sprintf(`groups.map(g, "%s" + g)`, transformationGroupsPrefix)}, nil) - require.NoError(t, err) - prefixUsernameAndGroupsPipeline.AppendTransformation(compiledTransform) - - compiledTransform, err = transformer.CompileTransformation(&celtransformer.AllowAuthenticationPolicy{Expression: `username == "someone-special"`}, nil) - require.NoError(t, err) - rejectAuthPipeline.AppendTransformation(compiledTransform) + prefixUsernameAndGroupsPipeline := transformtestutil.NewPrefixingPipeline(t, transformationUsernamePrefix, transformationGroupsPrefix) + rejectAuthPipeline := transformtestutil.NewRejectAllAuthPipeline(t) type testCase struct { name string diff --git a/internal/federationdomain/endpoints/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go index 08ce3d3a8..f903d99f7 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -28,6 +28,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/nonce" oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -65,6 +66,9 @@ const ( downstreamPKCEChallengeMethod = "S256" htmlContentType = "text/html; charset=utf-8" + + transformationUsernamePrefix = "username_prefix:" + transformationGroupsPrefix = "groups_prefix:" ) var ( @@ -102,13 +106,13 @@ var ( UpstreamSubject: oidcUpstreamSubject, }, } - happyDownstreamCustomSessionDataWithUsernameAndGroups = func(wantUsername string, wantGroups []string) *psession.CustomSessionData { + happyDownstreamCustomSessionDataWithUsernameAndGroups = func(wantDownstreamUsername, wantUpstreamUsername string, wantUpstreamGroups []string) *psession.CustomSessionData { copyOfCustomSession := *happyDownstreamCustomSessionData copyOfOIDC := *(happyDownstreamCustomSessionData.OIDC) copyOfCustomSession.OIDC = ©OfOIDC - copyOfCustomSession.Username = wantUsername - copyOfCustomSession.UpstreamUsername = wantUsername - copyOfCustomSession.UpstreamGroups = wantGroups + copyOfCustomSession.Username = wantDownstreamUsername + copyOfCustomSession.UpstreamUsername = wantUpstreamUsername + copyOfCustomSession.UpstreamGroups = wantUpstreamGroups return ©OfCustomSession } happyDownstreamAccessTokenCustomSessionData = &psession.CustomSessionData{ @@ -172,6 +176,9 @@ func TestCallbackEndpoint(t *testing.T) { require.NoError(t, kubeClient.Tracker().Add(secret)) } + prefixUsernameAndGroupsPipeline := transformtestutil.NewPrefixingPipeline(t, transformationUsernamePrefix, transformationGroupsPrefix) + rejectAuthPipeline := transformtestutil.NewRejectAllAuthPipeline(t) + tests := []struct { name string @@ -439,7 +446,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, nil), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, + oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, + nil, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -465,7 +476,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe@whitehouse.gov", oidcUpstreamGroupMembership), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + "joe@whitehouse.gov", + "joe@whitehouse.gov", + oidcUpstreamGroupMembership, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -493,7 +508,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe@whitehouse.gov", oidcUpstreamGroupMembership), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + "joe@whitehouse.gov", + "joe@whitehouse.gov", + oidcUpstreamGroupMembership, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -522,7 +541,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups("joe", oidcUpstreamGroupMembership), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + "joe", + "joe", + oidcUpstreamGroupMembership, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -653,7 +676,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamSubject, oidcUpstreamGroupMembership), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + oidcUpstreamSubject, + oidcUpstreamSubject, + oidcUpstreamGroupMembership, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -679,7 +706,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, []string{"notAnArrayGroup1 notAnArrayGroup2"}), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + oidcUpstreamUsername, + oidcUpstreamUsername, + []string{"notAnArrayGroup1 notAnArrayGroup2"}, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -705,7 +736,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, []string{"group1", "group2"}), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + oidcUpstreamUsername, + oidcUpstreamUsername, + []string{"group1", "group2"}, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -857,6 +892,35 @@ func TestCallbackEndpoint(t *testing.T) { args: happyExchangeAndValidateTokensArgs, }, }, + { + name: "using identity transformations which modify the username and group names", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(happyUpstream().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + method: http.MethodGet, + path: newRequestPath().WithState(happyState).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusSeeOther, + wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, + wantBody: "", + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + oidcUpstreamUsername, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, oidcUpstreamGroupMembership), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClientID: downstreamPinnipedClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + transformationUsernamePrefix+oidcUpstreamUsername, + oidcUpstreamUsername, + oidcUpstreamGroupMembership, + ), + wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ + performedByUpstreamName: happyUpstreamIDPName, + args: happyExchangeAndValidateTokensArgs, + }, + }, // Pre-upstream-exchange verification { @@ -1168,7 +1232,7 @@ func TestCallbackEndpoint(t *testing.T) { }, }, { - name: "the OIDCIdentityProvider CRD has been deleted", + name: "the OIDCIdentityProvider resource has been deleted", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(otherUpstreamOIDCIdentityProvider), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), @@ -1260,7 +1324,11 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups(oidcUpstreamUsername, nil), + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + oidcUpstreamUsername, + oidcUpstreamUsername, + nil, + ), wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -1442,7 +1510,23 @@ func TestCallbackEndpoint(t *testing.T) { args: happyExchangeAndValidateTokensArgs, }, }, + { + name: "using identity transformations which reject the authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(happyUpstream().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + method: http.MethodGet, + path: newRequestPath().WithState(happyState).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, + wantBody: "Unprocessable Entity: configured identity policy rejected this authentication: authentication was rejected by a configured policy\n", + wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ + performedByUpstreamName: happyUpstreamIDPName, + args: happyExchangeAndValidateTokensArgs, + }, + }, } + for _, test := range tests { test := test diff --git a/internal/testutil/transformtestutil/transformtestutil.go b/internal/testutil/transformtestutil/transformtestutil.go new file mode 100644 index 000000000..63576a184 --- /dev/null +++ b/internal/testutil/transformtestutil/transformtestutil.go @@ -0,0 +1,58 @@ +// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package transformtestutil + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/celtransformer" + "go.pinniped.dev/internal/idtransform" +) + +func NewPrefixingPipeline(t *testing.T, usernamePrefix, groupsPrefix string) *idtransform.TransformationPipeline { + t.Helper() + + transformer, err := celtransformer.NewCELTransformer(5 * time.Second) + require.NoError(t, err) + + p := idtransform.NewTransformationPipeline() + + userTransform, err := transformer.CompileTransformation( + &celtransformer.UsernameTransformation{Expression: fmt.Sprintf(`"%s" + username`, usernamePrefix)}, + nil, + ) + require.NoError(t, err) + p.AppendTransformation(userTransform) + + groupsTransform, err := transformer.CompileTransformation( + &celtransformer.GroupsTransformation{Expression: fmt.Sprintf(`groups.map(g, "%s" + g)`, groupsPrefix)}, + nil, + ) + require.NoError(t, err) + p.AppendTransformation(groupsTransform) + + return p +} + +func NewRejectAllAuthPipeline(t *testing.T) *idtransform.TransformationPipeline { + t.Helper() + + transformer, err := celtransformer.NewCELTransformer(5 * time.Second) + require.NoError(t, err) + + p := idtransform.NewTransformationPipeline() + + compiledTransform, err := transformer.CompileTransformation( + &celtransformer.AllowAuthenticationPolicy{Expression: `false`}, + nil, + ) + require.NoError(t, err) + p.AppendTransformation(compiledTransform) + + return p +} From 7f70fcf6791808053161fa699852557f2b4e9066 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 23 Aug 2023 11:12:23 -0700 Subject: [PATCH 70/81] add units tests to post_login_handler_test.go --- .../login/post_login_handler_test.go | 125 +++++++++++++++++- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/internal/federationdomain/endpoints/login/post_login_handler_test.go b/internal/federationdomain/endpoints/login/post_login_handler_test.go index b5c1c14d7..0a1753336 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/post_login_handler_test.go @@ -27,6 +27,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/transformtestutil" ) func TestPostLoginEndpoint(t *testing.T) { @@ -61,6 +62,9 @@ func TestPostLoginEndpoint(t *testing.T) { passParam = "password" badUserPassErrParamValue = "login_error" internalErrParamValue = "internal_error" + + transformationUsernamePrefix = "username_prefix:" + transformationGroupsPrefix = "groups_prefix:" ) var ( @@ -87,6 +91,12 @@ func TestPostLoginEndpoint(t *testing.T) { "error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed.", "state": happyDownstreamState, } + + fositeConfiguredPolicyRejectedAuthErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Reason: configured identity policy rejected this authentication: authentication was rejected by a configured policy.", + "state": happyDownstreamState, + } ) happyDownstreamScopesRequested := []string{"openid", "username", "groups"} @@ -172,19 +182,21 @@ func TestPostLoginEndpoint(t *testing.T) { return nil, false, nil } - upstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + upstreamLDAPIdentityProviderBuilder := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(parsedUpstreamLDAPURL). - WithAuthenticateFunc(ldapAuthenticateFunc). - Build() + WithAuthenticateFunc(ldapAuthenticateFunc) + + upstreamLDAPIdentityProvider := upstreamLDAPIdentityProviderBuilder.Build() - upstreamActiveDirectoryIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + upstreamActiveDirectoryIdentityProviderBuilder := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID(activeDirectoryUpstreamResourceUID). WithURL(parsedUpstreamLDAPURL). - WithAuthenticateFunc(ldapAuthenticateFunc). - Build() + WithAuthenticateFunc(ldapAuthenticateFunc) + + upstreamActiveDirectoryIdentityProvider := upstreamActiveDirectoryIdentityProviderBuilder.Build() erroringUpstreamLDAPIdentityProvider := oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). @@ -224,6 +236,26 @@ func TestPostLoginEndpoint(t *testing.T) { ActiveDirectory: nil, } + withUsernameAndGroupsInCustomSession := func(expectedCustomSessionData *psession.CustomSessionData, wantDownstreamUsername string, wantUpstreamUsername string, wantUpstreamGroups []string) *psession.CustomSessionData { + copyOfCustomSession := *expectedCustomSessionData + if expectedCustomSessionData.LDAP != nil { + copyOfLDAP := *(expectedCustomSessionData.LDAP) + copyOfCustomSession.LDAP = ©OfLDAP + } + if expectedCustomSessionData.OIDC != nil { + copyOfOIDC := *(expectedCustomSessionData.OIDC) + copyOfCustomSession.OIDC = ©OfOIDC + } + if expectedCustomSessionData.ActiveDirectory != nil { + copyOfActiveDirectory := *(expectedCustomSessionData.ActiveDirectory) + copyOfCustomSession.ActiveDirectory = ©OfActiveDirectory + } + copyOfCustomSession.Username = wantDownstreamUsername + copyOfCustomSession.UpstreamUsername = wantUpstreamUsername + copyOfCustomSession.UpstreamGroups = wantUpstreamGroups + return ©OfCustomSession + } + // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState @@ -252,6 +284,9 @@ func TestPostLoginEndpoint(t *testing.T) { require.NoError(t, kubeClient.Tracker().Add(secret)) } + prefixUsernameAndGroupsPipeline := transformtestutil.NewPrefixingPipeline(t, transformationUsernamePrefix, transformationGroupsPrefix) + rejectAuthPipeline := transformtestutil.NewRejectAllAuthPipeline(t) + tests := []struct { name string idps *oidctestutil.UpstreamIDPListerBuilder @@ -315,6 +350,34 @@ func TestPostLoginEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, + { + name: "happy LDAP login with identity transformations which modify the username and group names", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(upstreamLDAPIdentityProviderBuilder.WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()). // should pick this one + WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClient: downstreamPinnipedCLIClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyLDAPUpstreamCustomSession, + transformationUsernamePrefix+happyLDAPUsernameFromAuthenticator, + happyLDAPUsernameFromAuthenticator, + happyLDAPGroups, + ), + }, { name: "happy LDAP login with dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder(). @@ -362,6 +425,34 @@ func TestPostLoginEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, }, + { + name: "happy AD login with identity transformations which modify the username and group names", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(erroringUpstreamLDAPIdentityProvider). + WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder.WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), // should pick this one + decodedState: happyActiveDirectoryDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClient: downstreamPinnipedCLIClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: withUsernameAndGroupsInCustomSession( + expectedHappyActiveDirectoryUpstreamCustomSession, + transformationUsernamePrefix+happyLDAPUsernameFromAuthenticator, + happyLDAPUsernameFromAuthenticator, + happyLDAPGroups, + ), + }, { name: "happy AD login with dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder(). @@ -870,6 +961,28 @@ func TestPostLoginEndpoint(t *testing.T) { wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error }, + { + name: "LDAP login using identity transformations which reject the authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(upstreamLDAPIdentityProviderBuilder.WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + decodedState: happyLDAPDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeConfiguredPolicyRejectedAuthErrorQuery), + }, + { + name: "AD login using identity transformations which reject the authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder.WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + decodedState: happyActiveDirectoryDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeConfiguredPolicyRejectedAuthErrorQuery), + }, { name: "downstream state does not have enough entropy", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), From 5ad7e9a8ca0fab507b4604102d9a9af9c9e3c3e4 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 23 Aug 2023 16:01:25 -0700 Subject: [PATCH 71/81] started add units tests for identity transforms to token_handler_test.go --- .../endpoints/token/token_handler.go | 3 +- .../endpoints/token/token_handler_test.go | 86 +++++++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index c0f22ea53..19057685f 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -186,7 +186,8 @@ func upstreamOIDCRefresh( } mergedClaims := validatedTokens.IDToken.Claims - // To the extent possible, check that the user's basic identity hasn't changed. + // To the extent possible, check that the user's basic identity hasn't changed. We check that their downstream + // username has not changed separately below, as part of reapplying the transformations. err = validateSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims, session) if err != nil { return err diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 1ce813111..9b424cf31 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -61,6 +61,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/oidctypes" ) @@ -1749,6 +1750,9 @@ func TestRefreshGrant(t *testing.T) { activeDirectoryUpstreamResourceUID = "ad-resource-uid" activeDirectoryUpstreamType = "activedirectory" activeDirectoryUpstreamDN = "some-ad-user-dn" + + transformationUsernamePrefix = "username_prefix:" + transformationGroupsPrefix = "groups_prefix:" ) ldapUpstreamURL, _ := url.Parse("some-url") @@ -1861,6 +1865,13 @@ func TestRefreshGrant(t *testing.T) { return want } + happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownsteamGroups []string) tokenEndpointResponseExpectedValues { + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) + want.wantUsername = wantDownstreamUsername + want.wantGroups = wantDownsteamGroups + return want + } + withWantDynamicClientID := func(w tokenEndpointResponseExpectedValues) tokenEndpointResponseExpectedValues { w.wantClientID = dynamicClientID return w @@ -1895,6 +1906,12 @@ func TestRefreshGrant(t *testing.T) { return want } + happyRefreshTokenResponseForLDAPWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownsteamGroups []string) tokenEndpointResponseExpectedValues { + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownsteamGroups) + want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() + return want + } + happyRefreshTokenResponseForActiveDirectory := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues { want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) want.wantUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall() @@ -1945,10 +1962,20 @@ func TestRefreshGrant(t *testing.T) { }, } + happyLDAPCustomSessionDataWithUsername := func(wantDownstreamUsername string) *psession.CustomSessionData { + copyOfCustomSession := *happyLDAPCustomSessionData + copyOfLDAP := *(happyLDAPCustomSessionData.LDAP) + copyOfCustomSession.LDAP = ©OfLDAP + copyOfCustomSession.Username = wantDownstreamUsername + return ©OfCustomSession + } + happyAuthcodeExchangeInputsForOIDCUpstream := authcodeExchangeInputs{ - customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, - want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + initialUpstreamOIDCRefreshTokenCustomSessionData(), + ), } happyAuthcodeExchangeInputsForLDAPUpstream := authcodeExchangeInputs{ @@ -1959,6 +1986,8 @@ func TestRefreshGrant(t *testing.T) { ), } + prefixUsernameAndGroupsPipeline := transformtestutil.NewPrefixingPipeline(t, transformationUsernamePrefix, transformationGroupsPrefix) + tests := []struct { name string idps *oidctestutil.UpstreamIDPListerBuilder @@ -3498,6 +3527,39 @@ func TestRefreshGrant(t *testing.T) { ), }, }, + { + name: "upstream ldap refresh happy path with identity transformations which modify the username and group names", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline). + Build(), + ), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: happyLDAPCustomSessionDataWithUsername(transformationUsernamePrefix + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + session.IDTokenClaims().Extra["username"] = transformationUsernamePrefix + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + happyLDAPCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: happyRefreshTokenResponseForLDAPWithUsernameAndGroups( + happyLDAPCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + }, { name: "upstream ldap refresh happy path using dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). @@ -4436,7 +4498,11 @@ func (s *singleUseJWKProvider) GetJWKS(issuerName string) (jwks *jose.JSONWebKey return s.DynamicJWKSProvider.GetJWKS(issuerName) } -// Simulate the auth endpoint running so Fosite code will fill the store with realistic values. +// Simulate the results of the auth endpoint (and possibly also the related callback or login endpoints) by getting +// fosite's code to fill the session store with realistic values. Regardless of the specific flow that the user uses to +// become authorized, all authorization flows conclude with the user's identity saved into a fosite session and an +// authorization code being issued to the client. So the goal of this function is to save the user's identity into a +// session in the same way that the production code for those other endpoints would have done it. func simulateAuthEndpointHavingAlreadyRun( t *testing.T, authRequest *http.Request, @@ -4459,9 +4525,6 @@ func simulateAuthEndpointHavingAlreadyRun( }, Custom: initialCustomSessionData, } - if modifySession != nil { - modifySession(session) - } authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) require.NoError(t, err) @@ -4475,9 +4538,13 @@ func simulateAuthEndpointHavingAlreadyRun( authRequester.GrantScope("pinniped:request-audience") } + // Set the downstream username and group names that normally would have been determined by the authorize and related + // endpoints. These are stored into the fosite "extra" claims by the other endpoints, and when the token endpoint is + // called later, it will be able to find this information inside the "extra" claims in the session. // The authorization endpoint makes a special exception for the pinniped-cli client for backwards compatibility // and grants the username and groups scopes to that client even if it did not ask for them. Simulate that - // behavior here too. + // behavior here too by always adding these extras when the client_id is the Pinniped CLI client. + // Note that these (and anything else in the session) can be overridden by the modifySession param. if strings.Contains(authRequest.Form.Get("scope"), "username") || authRequest.Form.Get("client_id") == pinnipedCLIClientID { authRequester.GrantScope("username") session.Fosite.Claims.Extra["username"] = goodUsername @@ -4490,6 +4557,11 @@ func simulateAuthEndpointHavingAlreadyRun( // The authorization endpoint sets the authorized party to the client ID of the original requester. session.Fosite.Claims.Extra["azp"] = authRequester.GetClient().GetID() + // Allow some tests to further modify the session before it is stored. + if modifySession != nil { + modifySession(session) + } + authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session) require.NoError(t, err) return authResponder From 593d55ec09c42a7f1b79060973a66a1a722b00ca Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 29 Aug 2023 11:31:11 -0700 Subject: [PATCH 72/81] run codegen again after rebasing main branch into feature branch --- generated/1.21/README.adoc | 26 +- generated/1.22/README.adoc | 26 +- generated/1.23/README.adoc | 26 +- generated/1.24/README.adoc | 26 +- generated/1.25/README.adoc | 26 +- generated/1.26/README.adoc | 26 +- generated/1.27/README.adoc | 26 +- generated/1.28/README.adoc | 153 +++++++- .../config/v1alpha1/types_federationdomain.go | 221 +++++++++-- .../config/v1alpha1/types_oidcclient.go | 12 +- .../config/v1alpha1/zz_generated.deepcopy.go | 149 +++++++- ...rvisor.pinniped.dev_federationdomains.yaml | 354 +++++++++++++++++- 12 files changed, 913 insertions(+), 158 deletions(-) diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 4710a9b7f..1d00a56fb 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -673,6 +673,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -728,24 +740,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 178c5bfcd..89a75b6cf 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -673,6 +673,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -728,24 +740,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index 7044b5f46..dfa40dbdd 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -673,6 +673,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -728,24 +740,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index 5d92b7054..0be5d024b 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -673,6 +673,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -728,24 +740,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index eab5d0d14..dfb512f97 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -671,6 +671,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -726,24 +738,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index 22bbcf2ac..50823828c 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -671,6 +671,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -726,24 +738,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 1f0e01aeb..8fb2e935e 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -671,6 +671,18 @@ FederationDomainIdentityProvider describes how an identity provider is made avai +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainsecrets"] ==== FederationDomainSecrets @@ -726,24 +738,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`phase`* __FederationDomainPhase__ | Phase summarizes the overall status of the FederationDomain. +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. | *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index 08db55da5..d6ae00e91 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -650,6 +650,37 @@ FederationDomain describes the configuration of an OIDC provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] +==== FederationDomainIdentityProvider + +FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`displayName`* __string__ | DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a disruptive change for those users. +| *`objectRef`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. If the reference cannot be resolved then the identity provider will not be made available. Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider. +| *`transforms`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$]__ | Transforms is an optional way to specify transformations to be applied during user authentication and session refresh. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainphase"] +==== FederationDomainPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainsecrets"] @@ -687,7 +718,10 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | Field | Description | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. + For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -704,25 +738,12 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr [cols="25a,75a", options="header"] |=== | Field | Description -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainstatuscondition[$$FederationDomainStatusCondition$$]__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. -| *`message`* __string__ | Message provides human-readable details about the Status. -| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainstatuscondition"] -==== FederationDomainStatusCondition (string) - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] -**** - - - [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] ==== FederationDomainTLSSpec @@ -744,6 +765,106 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms"] +==== FederationDomainTransforms + +FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. + Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. +| *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] +==== FederationDomainTransformsConstant + +FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| 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. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] +==== FederationDomainTransformsExample + +FederationDomainTransformsExample defines a transform example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the input username. +| *`groups`* __string array__ | Groups is the input list of group names. +| *`expects`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects[$$FederationDomainTransformsExampleExpects$$]__ | Expects is the expected output of the entire sequence of transforms when they are run against the input Username and Groups. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] +==== FederationDomainTransformsExampleExpects + +FederationDomainTransformsExampleExpects defines the expected result for a transforms example. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username is the expected username after the transformations have been applied. +| *`groups`* __string array__ | Groups is the expected list of group names after the transformations have been applied. +| *`rejected`* __boolean__ | Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression after the transformations have been applied. True means that it is expected that the authentication would be rejected. The default value of false means that it is expected that the authentication would not be rejected by any policy expression. +| *`message`* __string__ | Message is the expected error message of the transforms. When Rejected is true, then Message is the expected message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, then Message will be treated as the default error message for authentication attempts which are rejected by a policy. When Rejected is false, then Message is the expected error message for some other non-policy transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] +==== FederationDomainTransformsExpression + +FederationDomainTransformsExpression defines a transform expression. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms[$$FederationDomainTransforms$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | Type determines the type of the expression. It must be one of the supported types. +| *`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. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-granttype"] ==== GrantType (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 27de4401c..e71ab0829 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-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -8,14 +8,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret -type FederationDomainStatusCondition string +type FederationDomainPhase string const ( - SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") - DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") - SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") - InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") + // FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. + FederationDomainPhasePending FederationDomainPhase = "Pending" + + // FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. + FederationDomainPhaseReady FederationDomainPhase = "Ready" + + // FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state. + FederationDomainPhaseError FederationDomainPhase = "Error" ) // FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. @@ -42,6 +45,157 @@ type FederationDomainTLSSpec struct { SecretName string `json:"secretName,omitempty"` } +// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to +// the transform expressions. This is a union type, and Type is the discriminator field. +type FederationDomainTransformsConstant struct { + // Name determines the name of the constant. It must be a valid identifier name. + // +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Name string `json:"name"` + + // Type determines the type of the constant, and indicates which other field should be non-empty. + // +kubebuilder:validation:Enum=string;stringList + Type string `json:"type"` + + // StringValue should hold the value when Type is "string", and is otherwise ignored. + // +optional + StringValue string `json:"stringValue,omitempty"` + + // StringListValue should hold the value when Type is "stringList", and is otherwise ignored. + // +optional + StringListValue []string `json:"stringListValue,omitempty"` +} + +// FederationDomainTransformsExpression defines a transform expression. +type FederationDomainTransformsExpression struct { + // Type determines the type of the expression. It must be one of the supported types. + // +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1 + Type string `json:"type"` + + // Expression is a CEL expression that will be evaluated based on the Type during an authentication. + // +kubebuilder:validation:MinLength=1 + Expression string `json:"expression"` + + // 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. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransformsExample defines a transform example. +type FederationDomainTransformsExample struct { + // Username is the input username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + + // Groups is the input list of group names. + // +optional + Groups []string `json:"groups,omitempty"` + + // Expects is the expected output of the entire sequence of transforms when they are run against the + // input Username and Groups. + Expects FederationDomainTransformsExampleExpects `json:"expects"` +} + +// FederationDomainTransformsExampleExpects defines the expected result for a transforms example. +type FederationDomainTransformsExampleExpects struct { + // Username is the expected username after the transformations have been applied. + // +optional + Username string `json:"username,omitempty"` + + // Groups is the expected list of group names after the transformations have been applied. + // +optional + Groups []string `json:"groups,omitempty"` + + // Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression + // after the transformations have been applied. True means that it is expected that the authentication would be + // rejected. The default value of false means that it is expected that the authentication would not be rejected + // by any policy expression. + // +optional + Rejected bool `json:"rejected,omitempty"` + + // Message is the expected error message of the transforms. When Rejected is true, then Message is the expected + // message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank, + // then Message will be treated as the default error message for authentication attempts which are rejected by a + // policy. When Rejected is false, then Message is the expected error message for some other non-policy + // transformation error, such as a runtime error. When Rejected is false, there is no default expected Message. + // +optional + Message string `json:"message,omitempty"` +} + +// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. +type FederationDomainTransforms struct { + // Constants defines constant variables and their values which will be made available to the transform expressions. + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + Constants []FederationDomainTransformsConstant `json:"constants,omitempty"` + + // Expressions are an optional list of transforms and policies to be executed in the order given during every + // authentication attempt, including during every session refresh. + // Each is a CEL expression. It may use the basic CEL language as defined in + // https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in + // https://github.com/google/cel-go/tree/master/ext#strings. + // + // The username and groups extracted from the identity provider, and the constants defined in this CR, are + // available as variables in all expressions. The username is provided via a variable called `username` and + // the list of group names is provided via a variable called `groups` (which may be an empty list). + // Each user-provided constants is provided via a variable named `strConst.varName` for string constants + // and `strListConst.varName` for string list constants. + // + // The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. + // Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated + // and the authentication attempt is rejected. + // Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the + // username or group names. + // Each username/v1 transform must return the new username (a string), which can be the same as the old username. + // Transformations of type username/v1 do not return group names, and therefore cannot change the group names. + // Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old + // groups list. + // Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. + // After each expression, the new (potentially changed) username or groups get passed to the following expression. + // + // Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. + // During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the + // authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username + // and group names have been decided for that authentication attempt. + // + // +optional + Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"` + + // Examples can optionally be used to ensure that the sequence of transformation expressions are working as + // expected. Examples define sample input identities which are then run through the expression list, and the + // results are compared to the expected results. If any example in this list fails, then this + // identity provider will not be available for use within this FederationDomain, and the error(s) will be + // added to the FederationDomain status. This can be used to help guard against programming mistakes in the + // expressions, and also act as living documentation for other administrators to better understand the expressions. + // +optional + Examples []FederationDomainTransformsExample `json:"examples,omitempty"` +} + +// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. +type FederationDomainIdentityProvider struct { + // DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the + // kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a + // disruptive change for those users. + // +kubebuilder:validation:MinLength=1 + DisplayName string `json:"displayName"` + + // ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required. + // If the reference cannot be resolved then the identity provider will not be made available. + // Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider, + // LDAPIdentityProvider, ActiveDirectoryIdentityProvider. + ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"` + + // Transforms is an optional way to specify transformations to be applied during user authentication and + // session refresh. + // +optional + Transforms FederationDomainTransforms `json:"transforms,omitempty"` +} + // FederationDomainSpec is a struct that describes an OIDC Provider. type FederationDomainSpec struct { // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the @@ -55,9 +209,35 @@ type FederationDomainSpec struct { // +kubebuilder:validation:MinLength=1 Issuer string `json:"issuer"` - // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. // +optional TLS *FederationDomainTLSSpec `json:"tls,omitempty"` + + // IdentityProviders is the list of identity providers available for use by this FederationDomain. + // + // An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, + // how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to + // extract a normalized user identity. Normalized user identities include a username and a list of group names. + // In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which + // belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations + // on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid + // accidental conflicts when multiple identity providers have different users with the same username (e.g. + // "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication + // rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow + // the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could + // disallow the authentication unless the user belongs to a specific group in the identity provider. + // + // For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, + // an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which + // exist in the same namespace, but also to reject all authentication requests when there is more than one identity + // provider currently defined. In this backwards compatibility mode, the name of the identity provider resource + // (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this + // FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of + // relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead + // explicitly list the identity provider using this IdentityProviders field. + // + // +optional + IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"` } // FederationDomainSecrets holds information about this OIDC Provider's secrets. @@ -86,20 +266,17 @@ type FederationDomainSecrets struct { // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. type FederationDomainStatus struct { - // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can - // represent success or failure. - // +optional - Status FederationDomainStatusCondition `json:"status,omitempty"` + // Phase summarizes the overall status of the FederationDomain. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase FederationDomainPhase `json:"phase,omitempty"` - // Message provides human-readable details about the Status. - // +optional - Message string `json:"message,omitempty"` - - // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get - // around some undesirable behavior with respect to the empty metav1.Time value (see - // https://github.com/kubernetes/kubernetes/issues/86811). - // +optional - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Conditions represent the observations of an FederationDomain's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional @@ -111,7 +288,7 @@ type FederationDomainStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:categories=pinniped // +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +kubebuilder:subresource:status type FederationDomain struct { diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go index 48f5de378..61106fdba 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -8,14 +8,14 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" type OIDCClientPhase string const ( - // PhasePending is the default phase for newly-created OIDCClient resources. - PhasePending OIDCClientPhase = "Pending" + // OIDCClientPhasePending is the default phase for newly-created OIDCClient resources. + OIDCClientPhasePending OIDCClientPhase = "Pending" - // PhaseReady is the phase for an OIDCClient resource in a healthy state. - PhaseReady OIDCClientPhase = "Ready" + // OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state. + OIDCClientPhaseReady OIDCClientPhase = "Ready" - // PhaseError is the phase for an OIDCClient in an unhealthy state. - PhaseError OIDCClientPhase = "Error" + // OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state. + OIDCClientPhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 77defc47c..0800faf56 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,24 @@ func (in *FederationDomain) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainIdentityProvider) DeepCopyInto(out *FederationDomainIdentityProvider) { + *out = *in + in.ObjectRef.DeepCopyInto(&out.ObjectRef) + in.Transforms.DeepCopyInto(&out.Transforms) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainIdentityProvider. +func (in *FederationDomainIdentityProvider) DeepCopy() *FederationDomainIdentityProvider { + if in == nil { + return nil + } + out := new(FederationDomainIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { *out = *in @@ -102,6 +120,13 @@ func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { *out = new(FederationDomainTLSSpec) **out = **in } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]FederationDomainIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -118,9 +143,12 @@ func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime - *out = (*in).DeepCopy() + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Secrets = in.Secrets return @@ -152,6 +180,121 @@ func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransforms) DeepCopyInto(out *FederationDomainTransforms) { + *out = *in + if in.Constants != nil { + in, out := &in.Constants, &out.Constants + *out = make([]FederationDomainTransformsConstant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Expressions != nil { + in, out := &in.Expressions, &out.Expressions + *out = make([]FederationDomainTransformsExpression, len(*in)) + copy(*out, *in) + } + if in.Examples != nil { + in, out := &in.Examples, &out.Examples + *out = make([]FederationDomainTransformsExample, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransforms. +func (in *FederationDomainTransforms) DeepCopy() *FederationDomainTransforms { + if in == nil { + return nil + } + out := new(FederationDomainTransforms) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsConstant) DeepCopyInto(out *FederationDomainTransformsConstant) { + *out = *in + if in.StringListValue != nil { + in, out := &in.StringListValue, &out.StringListValue + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsConstant. +func (in *FederationDomainTransformsConstant) DeepCopy() *FederationDomainTransformsConstant { + if in == nil { + return nil + } + out := new(FederationDomainTransformsConstant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExample) DeepCopyInto(out *FederationDomainTransformsExample) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Expects.DeepCopyInto(&out.Expects) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExample. +func (in *FederationDomainTransformsExample) DeepCopy() *FederationDomainTransformsExample { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExample) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExampleExpects) DeepCopyInto(out *FederationDomainTransformsExampleExpects) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExampleExpects. +func (in *FederationDomainTransformsExampleExpects) DeepCopy() *FederationDomainTransformsExampleExpects { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExampleExpects) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTransformsExpression) DeepCopyInto(out *FederationDomainTransformsExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTransformsExpression. +func (in *FederationDomainTransformsExpression) DeepCopy() *FederationDomainTransformsExpression { + if in == nil { + return nil + } + out := new(FederationDomainTransformsExpression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { *out = *in 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 71f7370d1..28a7425e9 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -21,7 +21,7 @@ spec: - jsonPath: .spec.issuer name: Issuer type: string - - jsonPath: .status.status + - jsonPath: .status.phase name: Status type: string - jsonPath: .metadata.creationTimestamp @@ -47,6 +47,263 @@ spec: spec: description: Spec of the OIDC provider. properties: + identityProviders: + description: "IdentityProviders is the list of identity providers + available for use by this FederationDomain. \n An identity provider + CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes + how to connect to a server, how to talk in a specific protocol for + authentication, and how to use the schema of that server/protocol + to extract a normalized user identity. Normalized user identities + include a username and a list of group names. In contrast, IdentityProviders + describes how to use that normalized identity in those Kubernetes + clusters which belong to this FederationDomain. Each entry in IdentityProviders + can be configured with arbitrary transformations on that normalized + identity. For example, a transformation can add a prefix to all + usernames to help avoid accidental conflicts when multiple identity + providers have different users with the same username (e.g. \"idp1:ryan\" + versus \"idp2:ryan\"). Each entry in IdentityProviders can also + implement arbitrary authentication rejection policies. Even though + a user was able to authenticate with the identity provider, a policy + can disallow the authentication to the Kubernetes clusters that + belong to this FederationDomain. For example, a policy could disallow + the authentication unless the user belongs to a specific group in + the identity provider. \n For backwards compatibility with versions + of Pinniped which predate support for multiple identity providers, + an empty IdentityProviders list will cause the FederationDomain + to use all available identity providers which exist in the same + namespace, but also to reject all authentication requests when there + is more than one identity provider currently defined. In this backwards + compatibility mode, the name of the identity provider resource (e.g. + the Name of an OIDCIdentityProvider resource) will be used as the + name of the identity provider in this FederationDomain. This mode + is provided to make upgrading from older versions easier. However, + instead of relying on this backwards compatibility mode, please + consider this mode to be deprecated and please instead explicitly + list the identity provider using this IdentityProviders field." + items: + description: FederationDomainIdentityProvider describes how an identity + provider is made available in this FederationDomain. + properties: + displayName: + description: DisplayName is the name of this identity provider + as it will appear to clients. This name ends up in the kubeconfig + of end users, so changing the name of an identity provider + that is in use by end users will be a disruptive change for + those users. + minLength: 1 + type: string + objectRef: + description: ObjectRef is a reference to a Pinniped identity + provider resource. A valid reference is required. If the reference + cannot be resolved then the identity provider will not be + made available. Must refer to a resource of one of the Pinniped + identity provider types, e.g. OIDCIdentityProvider, LDAPIdentityProvider, + ActiveDirectoryIdentityProvider. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + transforms: + description: Transforms is an optional way to specify transformations + to be applied during user authentication and session refresh. + properties: + constants: + description: Constants defines constant variables and their + values which will be made available to the transform expressions. + items: + description: FederationDomainTransformsConstant defines + a constant variable and its value which will be made + available to the transform expressions. This is a union + type, and Type is the discriminator field. + properties: + name: + description: Name determines the name of the constant. + It must be a valid identifier name. + maxLength: 64 + minLength: 1 + pattern: ^[a-zA-Z][_a-zA-Z0-9]*$ + type: string + stringListValue: + description: StringListValue should hold the value + when Type is "stringList", and is otherwise ignored. + items: + type: string + type: array + stringValue: + description: StringValue should hold the value when + 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. + enum: + - string + - stringList + type: string + required: + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + examples: + description: Examples can optionally be used to ensure that + the sequence of transformation expressions are working + as expected. Examples define sample input identities which + are then run through the expression list, and the results + are compared to the expected results. If any example in + this list fails, then this identity provider will not + be available for use within this FederationDomain, and + the error(s) will be added to the FederationDomain status. + This can be used to help guard against programming mistakes + in the expressions, and also act as living documentation + for other administrators to better understand the expressions. + items: + description: FederationDomainTransformsExample defines + a transform example. + properties: + expects: + description: Expects is the expected output of the + entire sequence of transforms when they are run + against the input Username and Groups. + properties: + groups: + description: Groups is the expected list of group + names after the transformations have been applied. + items: + type: string + type: array + message: + description: Message is the expected error message + of the transforms. When Rejected is true, then + Message is the expected message for the policy + which rejected the authentication attempt. When + Rejected is true and Message is blank, then + Message will be treated as the default error + message for authentication attempts which are + rejected by a policy. When Rejected is false, + then Message is the expected error message for + some other non-policy transformation error, + such as a runtime error. When Rejected is false, + there is no default expected Message. + type: string + rejected: + description: Rejected is a boolean that indicates + whether authentication is expected to be rejected + by a policy expression after the transformations + have been applied. True means that it is expected + that the authentication would be rejected. The + default value of false means that it is expected + that the authentication would not be rejected + by any policy expression. + type: boolean + username: + description: Username is the expected username + after the transformations have been applied. + type: string + type: object + groups: + description: Groups is the input list of group names. + items: + type: string + type: array + username: + description: Username is the input username. + minLength: 1 + type: string + required: + - expects + - username + type: object + type: array + expressions: + description: "Expressions are an optional list of transforms + and policies to be executed in the order given during + every authentication attempt, including during every session + refresh. Each is a CEL expression. It may use the basic + CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md + plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + \n The username and groups extracted from the identity + provider, and the constants defined in this CR, are available + as variables in all expressions. The username is provided + via a variable called `username` and the list of group + names is provided via a variable called `groups` (which + may be an empty list). Each user-provided constants is + provided via a variable named `strConst.varName` for string + constants and `strListConst.varName` for string list constants. + \n The only allowed types for expressions are currently + policy/v1, username/v1, and groups/v1. Each policy/v1 + must return a boolean, and when it returns false, no more + expressions from the list are evaluated and the authentication + attempt is rejected. Transformations of type policy/v1 + do not return usernames or group names, and therefore + cannot change the username or group names. Each username/v1 + transform must return the new username (a string), which + can be the same as the old username. Transformations of + type username/v1 do not return group names, and therefore + cannot change the group names. Each groups/v1 transform + must return the new groups list (list of strings), which + can be the same as the old groups list. Transformations + of type groups/v1 do not return usernames, and therefore + cannot change the usernames. After each expression, the + new (potentially changed) username or groups get passed + to the following expression. \n Any compilation or static + type-checking failure of any expression will cause an + error status on the FederationDomain. During an authentication + attempt, any unexpected runtime evaluation errors (e.g. + division by zero) cause the authentication attempt to + fail. When all expressions evaluate successfully, then + the (potentially changed) username and group names have + been decided for that authentication attempt." + items: + description: FederationDomainTransformsExpression defines + a transform expression. + properties: + expression: + description: Expression is a CEL expression that will + be evaluated based on the Type during an authentication. + minLength: 1 + type: string + message: + description: 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. + type: string + type: + description: Type determines the type of the expression. + It must be one of the supported types. + enum: + - policy/v1 + - username/v1 + - groups/v1 + type: string + required: + - expression + - type + type: object + type: array + type: object + required: + - displayName + - objectRef + type: object + type: array issuer: description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for @@ -59,8 +316,8 @@ spec: minLength: 1 type: string tls: - description: TLS configures how this FederationDomain is served over - Transport Layer Security (TLS). + description: TLS specifies a secret which will contain Transport Layer + Security (TLS) configuration for the FederationDomain. properties: secretName: description: "SecretName is an optional name of a Secret in the @@ -91,14 +348,80 @@ spec: status: description: Status of the OIDC provider. properties: - lastUpdateTime: - description: LastUpdateTime holds the time at which the Status was - last updated. It is a pointer to get around some undesirable behavior - with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). - format: date-time - type: string - message: - description: Message provides human-readable details about the Status. + conditions: + description: Conditions represent the observations of an FederationDomain's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the FederationDomain. + enum: + - Pending + - Ready + - Error type: string secrets: description: Secrets contains information about this OIDC Provider's @@ -145,15 +468,6 @@ spec: type: string type: object type: object - status: - description: Status holds an enum that describes the state of this - OIDC Provider. Note that this Status can represent success or failure. - enum: - - Success - - Duplicate - - Invalid - - SameIssuerHostMustUseSameSecret - type: string type: object required: - spec From 28210ab14d7e7678354a2b6e841d1972653ccd66 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 29 Aug 2023 14:14:46 -0700 Subject: [PATCH 73/81] add units tests to token_handler_test.go --- .../endpoints/token/token_handler_test.go | 263 +++++++++++++++++- 1 file changed, 261 insertions(+), 2 deletions(-) diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 9b424cf31..c444b7b0e 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -1782,6 +1782,12 @@ func TestRefreshGrant(t *testing.T) { } } + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername := func(downstreamUsername string) *psession.CustomSessionData { + customSessionData := initialUpstreamOIDCRefreshTokenCustomSessionData() + customSessionData.Username = downstreamUsername + return customSessionData + } + initialUpstreamOIDCAccessTokenCustomSessionData := func() *psession.CustomSessionData { return &psession.CustomSessionData{ Username: goodUsername, @@ -1804,6 +1810,12 @@ func TestRefreshGrant(t *testing.T) { return sessionData } + upstreamOIDCCustomSessionDataWithNewRefreshTokenWithUsername := func(newRefreshToken string, downstreamUsername string) *psession.CustomSessionData { + sessionData := initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(downstreamUsername) + sessionData.OIDC.UpstreamRefreshToken = newRefreshToken + return sessionData + } + happyOIDCUpstreamRefreshCall := func() *expectedUpstreamRefresh { return &expectedUpstreamRefresh{ performedByUpstreamName: oidcUpstreamName, @@ -1894,6 +1906,18 @@ func TestRefreshGrant(t *testing.T) { return want } + happyRefreshTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, expectToValidateToken *oauth2.Token, wantDownstreamUsername string, wantDownstreamGroups []string) tokenEndpointResponseExpectedValues { + // Should always have some custom session data stored. The other expectations happens to be the + // same as the same values as the authcode exchange case. + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownstreamGroups) + // Should always try to perform an upstream refresh. + want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall() + if expectToValidateToken != nil { + want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken, true) + } + return want + } + happyRefreshTokenResponseForOpenIDAndOfflineAccessWithAdditionalClaims := func(wantCustomSessionDataStored *psession.CustomSessionData, expectToValidateToken *oauth2.Token, wantAdditionalClaims map[string]interface{}) tokenEndpointResponseExpectedValues { want := happyRefreshTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored, expectToValidateToken) want.wantAdditionalClaims = wantAdditionalClaims @@ -1906,8 +1930,8 @@ func TestRefreshGrant(t *testing.T) { return want } - happyRefreshTokenResponseForLDAPWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownsteamGroups []string) tokenEndpointResponseExpectedValues { - want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownsteamGroups) + happyRefreshTokenResponseForLDAPWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownstreamGroups []string) tokenEndpointResponseExpectedValues { + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownstreamGroups) want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() return want } @@ -1987,6 +2011,7 @@ func TestRefreshGrant(t *testing.T) { } prefixUsernameAndGroupsPipeline := transformtestutil.NewPrefixingPipeline(t, transformationUsernamePrefix, transformationGroupsPrefix) + rejectAuthPipeline := transformtestutil.NewRejectAllAuthPipeline(t) tests := []struct { name string @@ -2014,6 +2039,160 @@ func TestRefreshGrant(t *testing.T) { ), }, }, + { + name: "happy path refresh grant with OIDC upstream with identity transformations which modify the username and group names", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ + IDToken: &oidctypes.IDToken{ + Claims: map[string]interface{}{ + "sub": goodUpstreamSubject, + }, + }, + }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). + WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + session.IDTokenClaims().Extra["username"] = transformationUsernamePrefix + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: happyRefreshTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + upstreamOIDCCustomSessionDataWithNewRefreshTokenWithUsername(oidcUpstreamRefreshedRefreshToken, transformationUsernamePrefix+goodUsername), + refreshedUpstreamTokensWithIDAndRefreshTokens(), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + }, + { + name: "happy path refresh grant with OIDC upstream with identity transformations which modify the username and group names when the upstream refresh does not return new username or groups then it reruns the transformations on the old upstream username and groups", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ + IDToken: &oidctypes.IDToken{ + Claims: map[string]interface{}{}, + }, + }).WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()). + WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + session.IDTokenClaims().Extra["username"] = transformationUsernamePrefix + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusOK, + wantClientID: pinnipedCLIClientID, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantUsername: transformationUsernamePrefix + goodUsername, + wantGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false), + wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshTokenWithUsername(oidcUpstreamRefreshedRefreshToken, transformationUsernamePrefix+goodUsername), + }, + }, + }, + { + name: "refresh grant with OIDC upstream with identity transformations which modify the username and group names when the downstream username has changed compared to initial login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ + IDToken: &oidctypes.IDToken{ + Claims: map[string]interface{}{ + "sub": goodUpstreamSubject, + }, + }, + }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). + WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername("some_other_transform_prefix:" + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + session.IDTokenClaims().Extra["username"] = "some_other_transform_prefix:" + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername("some_other_transform_prefix:"+goodUsername), + "some_other_transform_prefix:"+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh failed." + } + `), + }, + }, + }, + { + name: "refresh grant with OIDC upstream with identity transformations which reject the auth", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ + IDToken: &oidctypes.IDToken{ + Claims: map[string]interface{}{ + "sub": goodUpstreamSubject, + }, + }, + }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). + WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + session.IDTokenClaims().Extra["username"] = transformationUsernamePrefix + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh rejected by configured identity policy: authentication was rejected by a configured policy." + } + `), + }, + }, + }, { name: "happy path refresh grant with openid scope granted (id token returned) and additionalClaims", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( @@ -3560,6 +3739,86 @@ func TestRefreshGrant(t *testing.T) { ), }, }, + { + name: "upstream ldap refresh with identity transformations which modify the username and group names when the downstream username has changed compared to initial login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline). + Build(), + ), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: happyLDAPCustomSessionDataWithUsername("some_other_transform_prefix:" + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + // In this case we will simulate a successful auth so we can test what happens when the refresh is + // rejected by the identity transformations. + session.IDTokenClaims().Extra["username"] = "some_other_transform_prefix:" + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + happyLDAPCustomSessionDataWithUsername("some_other_transform_prefix:"+goodUsername), + "some_other_transform_prefix:"+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh failed." + } + `), + }, + }, + }, + { + name: "upstream ldap refresh with identity transformations which reject the auth", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + WithName(ldapUpstreamName). + WithResourceUID(ldapUpstreamResourceUID). + WithURL(ldapUpstreamURL). + WithPerformRefreshGroups(goodGroups). + WithTransformsForFederationDomain(rejectAuthPipeline). + Build(), + ), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: happyLDAPCustomSessionDataWithUsername(transformationUsernamePrefix + goodUsername), + modifySession: func(session *psession.PinnipedSession) { + // The authorization flow would have run the transformation pipeline and stored the transformed + // downstream identity in this part of the session, so simulate that by setting the expected result. + // In this case we will simulate a successful auth so we can test what happens when the refresh is + // rejected by the identity transformations. + session.IDTokenClaims().Extra["username"] = transformationUsernamePrefix + goodUsername + session.IDTokenClaims().Extra["groups"] = testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups) + }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups( + happyLDAPCustomSessionDataWithUsername(transformationUsernamePrefix+goodUsername), + transformationUsernamePrefix+goodUsername, + testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh rejected by configured identity policy: authentication was rejected by a configured policy." + } + `), + }, + }, + }, { name: "upstream ldap refresh happy path using dynamic client", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). From e2bdab9e2df7db60ba933ad82f6fa734cdeab01e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 30 Aug 2023 15:13:42 -0700 Subject: [PATCH 74/81] add the IDP display name to the downstream ID token's `sub` claim To make the subject of the downstream ID token more unique when there are multiple IDPs. It is possible to define two IDPs in a FederationDomain using the same identity provider CR, in which case the only thing that would make the subject claim different is adding the IDP display name into the values of the subject claim. --- hack/prepare-supervisor-on-kind.sh | 4 +- .../downstreamsession/downstream_session.go | 34 +- .../downstream_session_test.go | 116 ++++++ .../endpoints/auth/auth_handler.go | 10 +- .../endpoints/auth/auth_handler_test.go | 50 +-- .../endpoints/callback/callback_handler.go | 12 +- .../callback/callback_handler_test.go | 46 +-- .../endpoints/login/post_login_handler.go | 15 +- .../login/post_login_handler_test.go | 30 +- .../endpoints/token/token_handler.go | 2 +- .../upstreamprovider/upsteam_provider.go | 2 +- .../testutil/oidctestutil/oidctestutil.go | 2 +- internal/upstreamldap/upstreamldap.go | 4 +- internal/upstreamldap/upstreamldap_test.go | 10 +- test/integration/supervisor_login_test.go | 329 +++++++----------- 15 files changed, 372 insertions(+), 294 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 0696e17ba..1fdf4a814 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -246,7 +246,7 @@ if [[ "$use_oidc_upstream" == "yes" ]]; then fd_idps="${fd_idps}$( cat <(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -262,7 +262,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -290,7 +290,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -315,7 +315,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -339,7 +339,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -373,7 +373,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: []string{"openid"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -398,7 +398,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -437,7 +437,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenGroups: []string{}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -467,7 +467,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -499,7 +499,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -532,7 +532,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, // succeed despite `email_verified=false` because we're not using the email claim for anything wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -667,7 +667,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamSubject, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -697,7 +697,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"notAnArrayGroup1 notAnArrayGroup2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -727,7 +727,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"group1", "group2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -761,7 +761,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -791,7 +791,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -834,7 +834,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -877,7 +877,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -902,7 +902,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: transformationUsernamePrefix + oidcUpstreamUsername, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, oidcUpstreamGroupMembership), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -1158,7 +1158,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email", "username", "groups"}, wantDownstreamGrantedScopes: []string{"username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1188,7 +1188,7 @@ func TestCallbackEndpoint(t *testing.T) { wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email"}, // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantDownstreamGrantedScopes: []string{"username", "groups"}, @@ -1217,7 +1217,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1315,7 +1315,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamGrantedScopes: happyDownstreamScopesGranted, diff --git a/internal/federationdomain/endpoints/login/post_login_handler.go b/internal/federationdomain/endpoints/login/post_login_handler.go index 018ac4ffa..bdd1cb11c 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler.go +++ b/internal/federationdomain/endpoints/login/post_login_handler.go @@ -63,7 +63,9 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed } // Attempt to authenticate the user with the upstream IDP. - authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser(r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes()) + authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser( + r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes(), + ) if err != nil { plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.Provider.GetName()) // There was some problem during authentication with the upstream, aside from bad username/password. @@ -80,11 +82,15 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed // Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps. // Both success and error responses from this point onwards should look like the usual fosite redirect // responses, and a happy redirect response will include a downstream authcode. - subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream.Provider, authenticateResponse) + subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP( + ldapUpstream.Provider, authenticateResponse, ldapUpstream.DisplayName, + ) upstreamUsername := authenticateResponse.User.GetName() upstreamGroups := authenticateResponse.User.GetGroups() - username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), ldapUpstream.Transforms, upstreamUsername, upstreamGroups) + username, groups, err := downstreamsession.ApplyIdentityTransformations( + r.Context(), ldapUpstream.Transforms, upstreamUsername, upstreamGroups, + ) if err != nil { oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), false, @@ -92,7 +98,8 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed return nil } - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData( + ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{}) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) diff --git a/internal/federationdomain/endpoints/login/post_login_handler_test.go b/internal/federationdomain/endpoints/login/post_login_handler_test.go index 0a1753336..153cb9ded 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/post_login_handler_test.go @@ -338,7 +338,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -361,7 +361,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -390,7 +390,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -413,7 +413,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -436,7 +436,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -465,7 +465,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -490,7 +490,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyFormResponseRegexp: `(?s).*To finish logging in, paste this authorization code` + `.*
.*(.+).*`, // "(?s)" means match "." across newlines - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -515,7 +515,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -541,7 +541,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -567,7 +567,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, @@ -593,7 +593,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, @@ -627,7 +627,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "groups"}, @@ -661,7 +661,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username"}, @@ -691,7 +691,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"email"}, // only email was requested @@ -719,7 +719,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid"}, diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index 19057685f..53187d71a 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -394,7 +394,7 @@ func upstreamLDAPRefresh( Groups: oldUntransformedGroups, AdditionalAttributes: additionalAttributes, GrantedScopes: grantedScopes, - }) + }, p.DisplayName) if err != nil { return errUpstreamRefreshError().WithHint( "Upstream refresh failed.").WithTrace(err). diff --git a/internal/federationdomain/upstreamprovider/upsteam_provider.go b/internal/federationdomain/upstreamprovider/upsteam_provider.go index 68695b254..7caeafc1f 100644 --- a/internal/federationdomain/upstreamprovider/upsteam_provider.go +++ b/internal/federationdomain/upstreamprovider/upsteam_provider.go @@ -122,5 +122,5 @@ type UpstreamLDAPIdentityProviderI interface { authenticators.UserAuthenticator // PerformRefresh performs a refresh against the upstream LDAP identity provider - PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes) (groups []string, err error) + PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes, idpDisplayName string) (groups []string, err error) } diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index bffe5ae73..a3c55591a 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -220,7 +220,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { return u.URL } -func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { +func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, idpDisplayName string) ([]string, error) { if u.performRefreshArgs == nil { u.performRefreshArgs = make([]*PerformRefreshArgs, 0) } diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b178adf2e..ef1c432a6 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -188,7 +188,7 @@ func closeAndLogError(conn Conn, doingWhat string) { } } -func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { +func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, idpDisplayName string) ([]string, error) { t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches userDN := storedRefreshAttributes.DN @@ -237,7 +237,7 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes u if err != nil { return nil, err } - newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL()) + newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL(), idpDisplayName) if newSubject != storedRefreshAttributes.Subject { return nil, fmt.Errorf(`searching for user %q produced a different subject than the previous value. expected: %q, actual: %q`, userDN, storedRefreshAttributes.Subject, newSubject) } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index d07c2c9e1..e511d66e6 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -36,6 +36,7 @@ const ( testHost = "ldap.example.com:8443" testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" testBindPassword = "some-bind-password" + testUpstreamName = "some-upstream-idp-name" testUpstreamUsername = "some-upstream-username" testUpstreamPassword = "some-upstream-password" testUserSearchBase = "some-upstream-user-base-dn" @@ -2046,7 +2047,7 @@ func TestUpstreamRefresh(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) }, - wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"", + wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=some-upstream-idp-name&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=some-upstream-idp-name&sub=d3JvbmctdWlk\"", }, { name: "search result has wrong username", @@ -2279,14 +2280,17 @@ func TestUpstreamRefresh(t *testing.T) { } initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) ldapProvider := New(*tt.providerConfig) - subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" + subject := fmt.Sprintf( + "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=%s&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU", + testUpstreamName, + ) groups, err := ldapProvider.PerformRefresh(context.Background(), upstreamprovider.RefreshAttributes{ Username: testUserSearchResultUsernameAttributeValue, Subject: subject, DN: tt.refreshUserDN, AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded}, GrantedScopes: tt.grantedScopes, - }) + }, testUpstreamName) if tt.wantErr != "" { require.Error(t, err) require.Equal(t, tt.wantErr, err.Error()) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e7d25ecf4..0d4f2912c 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -162,6 +162,49 @@ func TestSupervisorLogin_Browser(t *testing.T) { return ldapIDP, secret } + // The downstream ID token Subject should include the upstream user ID after the upstream issuer name + // and IDP display name. + expectedIDTokenSubjectRegexForUpstreamOIDC := "^" + + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?idpName=test-upstream-oidc-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub=") + ".+" + + "$" + + // The downstream ID token Subject should be the Host URL, plus the user search base, plus the IDP display name, + // plus value pulled from the requested UserSearch.Attributes.UID attribute. + expectedIDTokenSubjectRegexForUpstreamLDAP := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ldap-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$" + + // Some of the LDAP tests below use the StartTLS server and port. + expectedIDTokenSubjectRegexForUpstreamLDAPStartTLSHost := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ldap-idp-") + `[\w]+` + + 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. + expectedIDTokenSubjectRegexForUpstreamAD := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$" + + // Sometimes the AD tests below use a different search base. + expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$" + + // Sometimes the AD tests below create a user dynamically so we can't know the UID up front. + expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub=") + ".+" + + "$" + // These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases. // They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an // OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go. @@ -275,8 +318,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { pinnipedSessionData := pinnipedSession.Custom pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + 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=") + ".+" }, }, @@ -299,7 +341,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { customSessionData := pinnipedSession.Custom customSessionData.Username = "some-incorrect-username" }, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { @@ -346,7 +388,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { customSessionData := pinnipedSession.Custom customSessionData.Username = "some-incorrect-username" }, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -375,8 +417,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + 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=") + ".+" }, }, @@ -406,8 +447,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + 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=") + ".+" }, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -431,9 +471,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { } return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + 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=") + ".+" }, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -472,12 +511,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -504,12 +538,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -534,12 +563,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -563,7 +587,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"}, wantDownstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username", "groups"}, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -579,13 +603,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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) + "$" @@ -634,13 +653,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials, + 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) + "$" @@ -684,12 +698,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -739,12 +748,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -778,12 +782,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.Username = "not-the-same" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAPStartTLSHost, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, @@ -852,12 +851,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -923,12 +917,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -957,12 +946,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.Username = "not-the-same" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + 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) + "$" @@ -1007,12 +991,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$" @@ -1055,12 +1034,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$" @@ -1112,12 +1086,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + 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) + "$" @@ -1184,12 +1153,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + 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) + "$" @@ -1218,8 +1182,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.ChangeADTestUserPassword(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1248,8 +1211,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.DeactivateADTestUser(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1278,8 +1240,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.LockADTestUser(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1337,12 +1298,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NoError(t, err) time.Sleep(10 * time.Second) // wait for controllers to pick up the change }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -1381,12 +1337,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { time.Sleep(10 * time.Second) // wait for controllers to pick up the change return []string{} }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + 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) + "$" @@ -1399,10 +1350,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "contains-disallowed-substring.pinniped.dev-something", // .pinniped.dev substring is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "contains-disallowed-substring.pinniped.dev-something", // .pinniped.dev substring is not allowed + 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=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1420,10 +1370,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "client.oauth.pinniped.dev-client-name", // OIDC dynamic client name is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "client.oauth.pinniped.dev-client-name", // OIDC dynamic client name is not allowed + 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=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1441,10 +1390,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "pinniped-cli", // pinniped-cli is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "pinniped-cli", // pinniped-cli is not allowed + 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=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1477,9 +1425,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, }, configv1alpha1.OIDCClientPhaseReady) }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -1510,9 +1457,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, }, configv1alpha1.OIDCClientPhaseReady) }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -1539,13 +1485,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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) + "$" @@ -1691,14 +1632,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username"}, // do not request (or expect) groups - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username"}, // do not request (or expect) groups + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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) + "$" @@ -1724,14 +1660,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "groups"}, // do not request (or expect) username - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "groups"}, // do not request (or expect) username + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "" // username should not exist as a claim since we did not request it }, @@ -1763,14 +1694,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "offline_access"}, // do not request (or expect) pinniped:request-audience or username or groups - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "offline_access"}, // do not request (or expect) pinniped:request-audience or username or groups + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "" // username should not exist as a claim since we did not request it }, @@ -1802,13 +1728,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login env.SupervisorUpstreamActiveDirectory.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + 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) + "$" @@ -1878,8 +1799,12 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, }, displayName }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer) + + regexp.QuoteMeta("?idpName="+url.QueryEscape("my oidc idp")) + + regexp.QuoteMeta("&sub=") + ".+" + + "$", wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamOIDC.Username) + "$" }, @@ -1958,12 +1883,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ldap idp")) + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -2023,12 +1947,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ldap idp")) + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -2093,12 +2016,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ad idp")) + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -2160,12 +2082,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ad idp")) + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" From b6f0dc3ba712246182048ab5b68c73ba467aed10 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 7 Sep 2023 09:52:56 -0700 Subject: [PATCH 75/81] Fix conflicts caused from rebasing main into multiple IDPs branch --- .../active_directory_upstream_watcher_test.go | 2 +- internal/upstreamldap/upstreamldap_test.go | 4 ++-- test/integration/e2e_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 f88287a01..92500d6eb 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -2491,7 +2491,7 @@ func TestAttributeUnchangedSinceLogin(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal)) - err := attributeUnchangedSinceLogin(attributeName)(tt.entry, provider.RefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}}) + err := attributeUnchangedSinceLogin(attributeName)(tt.entry, upstreamprovider.RefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}}) if tt.wantErr != "" { require.Error(t, err) require.Equal(t, tt.wantErr, err.Error()) diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index e511d66e6..574b478de 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -2203,8 +2203,8 @@ func TestUpstreamRefresh(t *testing.T) { { name: "search result has a changed pwdLastSet value", providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.RefreshAttributes) error{ - pwdLastSetAttribute: func(*ldap.Entry, provider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { return errors.New(`value for attribute "pwdLastSet" has changed since initial value at login`) }, } diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 48074af3b..f684b5123 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1210,7 +1210,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) - tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + tempDir := t.TempDir() // per-test tmp dir to avoid sharing files between tests // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. browser := browsertest.OpenBrowser(t) From a7bd494ec33a7d176f7a66cf1a423b8c41225765 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 11 Sep 2023 12:51:35 -0700 Subject: [PATCH 76/81] update FederationDomain.status.conditions to come from metav1 --- .../v1alpha1/types_federationdomain.go.tmpl | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.21/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.22/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.23/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.24/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.25/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.26/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.27/README.adoc | 52 +++--- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- generated/1.28/README.adoc | 26 +-- .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 2 +- ...rvisor.pinniped.dev_federationdomains.yaml | 12 +- .../v1alpha1/zz_generated.deepcopy.go | 1 + .../config/v1alpha1/zz_generated.deepcopy.go | 1 + .../v1alpha1/zz_generated.conversion.go | 1 + .../v1alpha1/zz_generated.deepcopy.go | 1 + .../v1alpha1/zz_generated.defaults.go | 1 + .../identity/zz_generated.deepcopy.go | 1 + .../login/v1alpha1/zz_generated.conversion.go | 1 + .../login/v1alpha1/zz_generated.deepcopy.go | 1 + .../login/v1alpha1/zz_generated.defaults.go | 1 + .../concierge/login/zz_generated.deepcopy.go | 1 + .../v1alpha1/zz_generated.conversion.go | 1 + .../v1alpha1/zz_generated.deepcopy.go | 1 + .../v1alpha1/zz_generated.defaults.go | 1 + .../clientsecret/zz_generated.deepcopy.go | 1 + .../config/v1alpha1/types_federationdomain.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 3 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 1 + .../openapi/zz_generated.openapi.go | 105 ++++++------ .../federation_domain_watcher.go | 142 ++++++++-------- .../federation_domain_watcher_test.go | 158 +++++++++--------- test/integration/supervisor_discovery_test.go | 34 ++-- ...supervisor_federationdomain_status_test.go | 26 +-- test/testlib/client.go | 2 +- 57 files changed, 545 insertions(+), 474 deletions(-) diff --git a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl index e71ab0829..95f7da282 100644 --- a/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index 28a7425e9..212569609 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 1d00a56fb..a27e6d3ce 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -653,7 +653,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -674,7 +674,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -721,8 +721,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -741,7 +741,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -768,7 +768,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -781,16 +781,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -810,7 +810,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -829,7 +829,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -849,7 +849,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -868,7 +868,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -902,7 +902,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -927,9 +927,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -954,7 +954,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -966,7 +966,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1286,7 +1286,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1454,7 +1454,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1619,7 +1619,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go index e71ab0829..95f7da282 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 28a7425e9..7ffd8875d 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + type FooStatus struct{ // Represents the observations of a foo's + current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 89a75b6cf..09c4ca182 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -653,7 +653,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -674,7 +674,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -721,8 +721,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -741,7 +741,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -768,7 +768,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -781,16 +781,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -810,7 +810,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -829,7 +829,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -849,7 +849,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -868,7 +868,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -902,7 +902,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -927,9 +927,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -954,7 +954,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -966,7 +966,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1286,7 +1286,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1454,7 +1454,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1619,7 +1619,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go index e71ab0829..95f7da282 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 28a7425e9..7ffd8875d 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + type FooStatus struct{ // Represents the observations of a foo's + current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index dfa40dbdd..255975ed3 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -653,7 +653,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -674,7 +674,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -721,8 +721,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -741,7 +741,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -768,7 +768,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -781,16 +781,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -810,7 +810,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -829,7 +829,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -849,7 +849,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -868,7 +868,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -902,7 +902,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -927,9 +927,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -954,7 +954,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -966,7 +966,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1286,7 +1286,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1454,7 +1454,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1619,7 +1619,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (string) diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go index e71ab0829..95f7da282 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 28a7425e9..212569609 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index 0be5d024b..7d524038e 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -455,7 +455,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -571,7 +571,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -583,7 +583,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -595,7 +595,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -653,7 +653,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -674,7 +674,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -721,8 +721,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -741,7 +741,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -768,7 +768,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -781,16 +781,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -810,7 +810,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -829,7 +829,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -849,7 +849,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -868,7 +868,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -902,7 +902,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -927,9 +927,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -954,7 +954,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -966,7 +966,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1286,7 +1286,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1454,7 +1454,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1619,7 +1619,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 e71ab0829..95f7da282 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 28a7425e9..212569609 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index dfb512f97..916d768b7 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -651,7 +651,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -672,7 +672,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -719,8 +719,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -739,7 +739,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -766,7 +766,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -779,16 +779,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -808,7 +808,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -827,7 +827,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -847,7 +847,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -866,7 +866,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -900,7 +900,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -925,9 +925,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -952,7 +952,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -964,7 +964,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1282,7 +1282,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1450,7 +1450,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1615,7 +1615,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 e71ab0829..95f7da282 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 28a7425e9..212569609 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index 50823828c..47b96cc6a 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -651,7 +651,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -672,7 +672,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -719,8 +719,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -739,7 +739,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -766,7 +766,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -779,16 +779,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -808,7 +808,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -827,7 +827,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -847,7 +847,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -866,7 +866,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -900,7 +900,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -925,9 +925,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -952,7 +952,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -964,7 +964,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1282,7 +1282,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1450,7 +1450,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1615,7 +1615,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 e71ab0829..95f7da282 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 28a7425e9..212569609 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 8fb2e935e..f34bc05b1 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -453,7 +453,7 @@ CredentialIssuerStrategy describes the status of an integration strategy that wa [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-frontendtype"] -==== FrontendType (string) +==== FrontendType (string) FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. @@ -569,7 +569,7 @@ ImpersonationProxyTLSSpec contains information about how the Concierge impersona [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategyreason"] -==== StrategyReason (string) +==== StrategyReason (string) StrategyReason enumerates the detailed reason why a strategy is in a particular status. @@ -581,7 +581,7 @@ StrategyReason enumerates the detailed reason why a strategy is in a particular [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategystatus"] -==== StrategyStatus (string) +==== StrategyStatus (string) StrategyStatus enumerates whether a strategy is working on a cluster. @@ -593,7 +593,7 @@ StrategyStatus enumerates whether a strategy is working on a cluster. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-config-v1alpha1-strategytype"] -==== StrategyType (string) +==== StrategyType (string) StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. @@ -651,7 +651,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -672,7 +672,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -719,8 +719,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -739,7 +739,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -766,7 +766,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -779,16 +779,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -808,7 +808,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -827,7 +827,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -847,7 +847,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. @@ -866,7 +866,7 @@ FederationDomainTransformsExpression defines a transform expression. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype"] -==== GrantType (string) +==== GrantType (string) @@ -900,7 +900,7 @@ OIDCClient describes the configuration of an OIDC client. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-oidcclientphase"] -==== OIDCClientPhase (string) +==== OIDCClientPhase (string) @@ -925,9 +925,9 @@ OIDCClientSpec is a struct that describes an OIDCClient. |=== | Field | Description | *`allowedRedirectURIs`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-redirecturi[$$RedirectURI$$] array__ | allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this client. Any other uris will be rejected. Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme. Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri. -| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. +| *`allowedGrantTypes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-granttype[$$GrantType$$] array__ | allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this client. Must only contain the following values: - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to authenticate users. This grant must always be listed. - refresh_token: allows the client to perform refresh grants for the user to extend the user's session. This grant must be listed if allowedScopes lists offline_access. - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. This grant must be listed if allowedScopes lists pinniped:request-audience. -| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. +| *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. |=== @@ -952,7 +952,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-redirecturi"] -==== RedirectURI (string) +==== RedirectURI (string) @@ -964,7 +964,7 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope"] -==== Scope (string) +==== Scope (string) @@ -1282,7 +1282,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderphase"] -==== ActiveDirectoryIdentityProviderPhase (string) +==== ActiveDirectoryIdentityProviderPhase (string) @@ -1450,7 +1450,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-ldapidentityproviderphase"] -==== LDAPIdentityProviderPhase (string) +==== LDAPIdentityProviderPhase (string) @@ -1615,7 +1615,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-oidcidentityproviderphase"] -==== OIDCIdentityProviderPhase (string) +==== OIDCIdentityProviderPhase (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 e71ab0829..95f7da282 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 28a7425e9..212569609 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index d6ae00e91..10a5f3a2d 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -651,7 +651,7 @@ FederationDomain describes the configuration of an OIDC provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider"] -==== FederationDomainIdentityProvider +==== FederationDomainIdentityProvider FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain. @@ -672,7 +672,7 @@ FederationDomainIdentityProvider describes how an identity provider is made avai [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainphase"] -==== FederationDomainPhase (string) +==== FederationDomainPhase (string) @@ -719,8 +719,8 @@ FederationDomainSpec is a struct that describes an OIDC Provider. | *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain. -| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. - An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. +| *`identityProviders`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainidentityprovider[$$FederationDomainIdentityProvider$$] array__ | IdentityProviders is the list of identity providers available for use by this FederationDomain. + An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server, how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to extract a normalized user identity. Normalized user identities include a username and a list of group names. In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid accidental conflicts when multiple identity providers have different users with the same username (e.g. "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could disallow the authentication unless the user belongs to a specific group in the identity provider. For backwards compatibility with versions of Pinniped which predate support for multiple identity providers, an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which exist in the same namespace, but also to reject all authentication requests when there is more than one identity provider currently defined. In this backwards compatibility mode, the name of the identity provider resource (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead explicitly list the identity provider using this IdentityProviders field. |=== @@ -739,7 +739,7 @@ FederationDomainStatus is a struct that describes the actual state of an OIDC Pr |=== | Field | Description | *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainphase[$$FederationDomainPhase$$]__ | Phase summarizes the overall status of the FederationDomain. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the observations of an FederationDomain's current state. | *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== @@ -766,7 +766,7 @@ FederationDomainTLSSpec is a struct that describes the TLS configuration for an [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransforms"] -==== FederationDomainTransforms +==== FederationDomainTransforms FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain. @@ -779,16 +779,16 @@ FederationDomainTransforms defines identity transformations for an identity prov |=== | Field | Description | *`constants`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant[$$FederationDomainTransformsConstant$$] array__ | Constants defines constant variables and their values which will be made available to the transform expressions. -| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. - The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. - The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. +| *`expressions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression[$$FederationDomainTransformsExpression$$] array__ | Expressions are an optional list of transforms and policies to be executed in the order given during every authentication attempt, including during every session refresh. Each is a CEL expression. It may use the basic CEL language as defined in https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in https://github.com/google/cel-go/tree/master/ext#strings. + The username and groups extracted from the identity provider, and the constants defined in this CR, are available as variables in all expressions. The username is provided via a variable called `username` and the list of group names is provided via a variable called `groups` (which may be an empty list). Each user-provided constants is provided via a variable named `strConst.varName` for string constants and `strListConst.varName` for string list constants. + The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1. Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated and the authentication attempt is rejected. Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the username or group names. Each username/v1 transform must return the new username (a string), which can be the same as the old username. Transformations of type username/v1 do not return group names, and therefore cannot change the group names. Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old groups list. Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames. After each expression, the new (potentially changed) username or groups get passed to the following expression. Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain. During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username and group names have been decided for that authentication attempt. | *`examples`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexample[$$FederationDomainTransformsExample$$] array__ | Examples can optionally be used to ensure that the sequence of transformation expressions are working as expected. Examples define sample input identities which are then run through the expression list, and the results are compared to the expected results. If any example in this list fails, then this identity provider will not be available for use within this FederationDomain, and the error(s) will be added to the FederationDomain status. This can be used to help guard against programming mistakes in the expressions, and also act as living documentation for other administrators to better understand the expressions. |=== [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsconstant"] -==== FederationDomainTransformsConstant +==== FederationDomainTransformsConstant FederationDomainTransformsConstant defines a constant variable and its value which will be made available to the transform expressions. This is a union type, and Type is the discriminator field. @@ -808,7 +808,7 @@ FederationDomainTransformsConstant defines a constant variable and its value whi [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexample"] -==== FederationDomainTransformsExample +==== FederationDomainTransformsExample FederationDomainTransformsExample defines a transform example. @@ -827,7 +827,7 @@ FederationDomainTransformsExample defines a transform example. [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexampleexpects"] -==== FederationDomainTransformsExampleExpects +==== FederationDomainTransformsExampleExpects FederationDomainTransformsExampleExpects defines the expected result for a transforms example. @@ -847,7 +847,7 @@ FederationDomainTransformsExampleExpects defines the expected result for a trans [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-federationdomaintransformsexpression"] -==== FederationDomainTransformsExpression +==== FederationDomainTransformsExpression FederationDomainTransformsExpression defines a transform expression. 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 e71ab0829..95f7da282 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index 0800faf56..018a30233 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -145,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 28a7425e9..212569609 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -352,9 +352,15 @@ spec: description: Conditions represent the observations of an FederationDomain's current state. items: - description: Condition status of a resource (mirrored from the metav1.Condition - type added in Kubernetes 1.19). In a future API version we can - switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition 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 a0e57b94e..2b36eaa88 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3373989..d4a01ba48 100644 --- a/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.conversion.go b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.conversion.go index 1786f06ad..700a55988 100644 --- a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.conversion.go +++ b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.conversion.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go index 533b24ce7..2feed25b2 100644 --- a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.defaults.go b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.defaults.go index 8d370915b..abdbfa5fb 100644 --- a/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.defaults.go +++ b/generated/latest/apis/concierge/identity/v1alpha1/zz_generated.defaults.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/identity/zz_generated.deepcopy.go b/generated/latest/apis/concierge/identity/zz_generated.deepcopy.go index e7ca4d6f8..840da45eb 100644 --- a/generated/latest/apis/concierge/identity/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/identity/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.conversion.go b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.conversion.go index 0461660e1..bc409d261 100644 --- a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.conversion.go +++ b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.conversion.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go index fb5263c8f..0d64c87a3 100644 --- a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.defaults.go b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.defaults.go index 8d370915b..abdbfa5fb 100644 --- a/generated/latest/apis/concierge/login/v1alpha1/zz_generated.defaults.go +++ b/generated/latest/apis/concierge/login/v1alpha1/zz_generated.defaults.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/concierge/login/zz_generated.deepcopy.go b/generated/latest/apis/concierge/login/zz_generated.deepcopy.go index ecf3a3c42..f332880e5 100644 --- a/generated/latest/apis/concierge/login/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/login/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.conversion.go b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.conversion.go index 692c322df..3c9326d59 100644 --- a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.conversion.go +++ b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.conversion.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.deepcopy.go index 0280f8ca6..cd9c904e4 100644 --- a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.defaults.go b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.defaults.go index 8d370915b..abdbfa5fb 100644 --- a/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.defaults.go +++ b/generated/latest/apis/supervisor/clientsecret/v1alpha1/zz_generated.defaults.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/supervisor/clientsecret/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/clientsecret/zz_generated.deepcopy.go index 49a60f3d1..4153df600 100644 --- a/generated/latest/apis/supervisor/clientsecret/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/clientsecret/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go index e71ab0829..95f7da282 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -276,7 +276,7 @@ type FederationDomainStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // Secrets contains information about this OIDC Provider's secrets. // +optional diff --git a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index e1af0fd0a..018a30233 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 @@ -144,7 +145,7 @@ func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } 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 4a1a2c033..ad0a38060 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/generated/latest/client/supervisor/openapi/zz_generated.openapi.go b/generated/latest/client/supervisor/openapi/zz_generated.openapi.go index d525b9065..6612e5488 100644 --- a/generated/latest/client/supervisor/openapi/zz_generated.openapi.go +++ b/generated/latest/client/supervisor/openapi/zz_generated.openapi.go @@ -1,4 +1,5 @@ //go:build !ignore_autogenerated +// +build !ignore_autogenerated // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 @@ -21,58 +22,58 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1.OIDCClientSecretRequestList": schema_apis_supervisor_clientsecret_v1alpha1_OIDCClientSecretRequestList(ref), "go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1.OIDCClientSecretRequestSpec": schema_apis_supervisor_clientsecret_v1alpha1_OIDCClientSecretRequestSpec(ref), "go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1.OIDCClientSecretRequestStatus": schema_apis_supervisor_clientsecret_v1alpha1_OIDCClientSecretRequestStatus(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), - "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), - "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), - "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), - "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), + "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), + "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), + "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), + "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), } } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 7f9f16e8f..23468115d 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -191,13 +191,13 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro func (c *federationDomainWatcherController) processAllFederationDomains( ctx context.Context, federationDomains []*configv1alpha1.FederationDomain, -) ([]*federationdomainproviders.FederationDomainIssuer, map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition, error) { +) ([]*federationdomainproviders.FederationDomainIssuer, map[*configv1alpha1.FederationDomain][]*metav1.Condition, error) { federationDomainIssuers := make([]*federationdomainproviders.FederationDomainIssuer, 0) - fdToConditionsMap := map[*configv1alpha1.FederationDomain][]*configv1alpha1.Condition{} + fdToConditionsMap := map[*configv1alpha1.FederationDomain][]*metav1.Condition{} crossDomainConfigValidator := newCrossFederationDomainConfigValidator(federationDomains) for _, federationDomain := range federationDomains { - conditions := make([]*configv1alpha1.Condition, 0) + conditions := make([]*metav1.Condition, 0) conditions = crossDomainConfigValidator.Validate(federationDomain, conditions) @@ -223,8 +223,8 @@ func (c *federationDomainWatcherController) processAllFederationDomains( func (c *federationDomainWatcherController) makeFederationDomainIssuer( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, - conditions []*configv1alpha1.Condition, -) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + conditions []*metav1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*metav1.Condition, error) { var err error // Create the list of IDPs for this FederationDomain. // Don't worry if the IDP CRs themselves is phase=Ready because those which are not ready will not be loaded @@ -247,8 +247,8 @@ func (c *federationDomainWatcherController) makeFederationDomainIssuer( func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( federationDomain *configv1alpha1.FederationDomain, - conditions []*configv1alpha1.Condition, -) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + conditions []*metav1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*metav1.Condition, error) { var defaultFederationDomainIdentityProvider *federationdomainproviders.FederationDomainIdentityProvider // When the FederationDomain does not list any IDPs, then we might be in backwards compatibility mode. @@ -290,9 +290,9 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( // Backwards compatibility mode always uses an empty identity transformation pipeline since no // transformations are defined on the FederationDomain. defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonLegacyConfigurationSuccess, Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+ "identity provider resource has been found: using %q as "+ @@ -300,9 +300,9 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( "(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName), }) case idpCRsCount > 1: - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonIdentityProviderNotSpecified, // vs LegacyConfigurationIdentityProviderNotFound as this is more specific Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+ "and %d identity provider resources have been found: "+ @@ -310,9 +310,9 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( "this federation domain should use", idpCRsCount), }) default: - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonLegacyConfigurationIdentityProviderNotFound, Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " + "resources have been found: please create an identity provider resource", @@ -338,8 +338,8 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( func (c *federationDomainWatcherController) makeFederationDomainIssuerWithExplicitIDPs( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, - conditions []*configv1alpha1.Condition, -) (*federationdomainproviders.FederationDomainIssuer, []*configv1alpha1.Condition, error) { + conditions []*metav1.Condition, +) (*federationdomainproviders.FederationDomainIssuer, []*metav1.Condition, error) { federationDomainIdentityProviders := []*federationdomainproviders.FederationDomainIdentityProvider{} idpNotFoundIndices := []int{} displayNames := sets.Set[string]{} @@ -640,19 +640,19 @@ func (c *federationDomainWatcherController) evaluateExamplesForIdentityProvider( return true, "" } -func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuffixNames []string, conditions []*metav1.Condition) []*metav1.Condition { if len(badSuffixNames) > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersObjectRefKindValid, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonKindUnrecognized, Message: fmt.Sprintf("some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized (should be one of %s): %s", strings.Join(expectedKinds, ", "), strings.Join(sortAndQuote(badSuffixNames), ", ")), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersObjectRefKindValid, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized", }) @@ -663,24 +663,24 @@ func appendIdentityProviderObjectRefKindCondition(expectedKinds []string, badSuf func appendIdentityProvidersFoundCondition( idpNotFoundIndices []int, federationDomainIdentityProviders []configv1alpha1.FederationDomainIdentityProvider, - conditions []*configv1alpha1.Condition, -) []*configv1alpha1.Condition { + conditions []*metav1.Condition, +) []*metav1.Condition { if len(idpNotFoundIndices) != 0 { messages := []string{} for _, idpNotFoundIndex := range idpNotFoundIndices { messages = append(messages, fmt.Sprintf("cannot find resource specified by .spec.identityProviders[%d].objectRef (with name %q)", idpNotFoundIndex, federationDomainIdentityProviders[idpNotFoundIndex].ObjectRef.Name)) } - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonIdentityProvidersObjectRefsNotFound, Message: strings.Join(messages, "\n\n"), }) } else if len(federationDomainIdentityProviders) != 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersFound, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the resources specified by .spec.identityProviders[].objectRef were found", }) @@ -688,19 +688,19 @@ func appendIdentityProvidersFoundCondition( return conditions } -func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName string, badSuffixNames []string, conditions []*metav1.Condition) []*metav1.Condition { if len(badSuffixNames) > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersAPIGroupSuffixValid, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonAPIGroupNameUnrecognized, Message: fmt.Sprintf("some API groups specified by .spec.identityProviders[].objectRef.apiGroup are not recognized (should be %q): %s", expectedSuffixName, strings.Join(sortAndQuote(badSuffixNames), ", ")), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersAPIGroupSuffixValid, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized", }) @@ -708,18 +708,18 @@ func appendIdentityProviderObjectRefAPIGroupSuffixCondition(expectedSuffixName s return conditions } -func appendTransformsExpressionsValidCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendTransformsExpressionsValidCondition(messages []string, conditions []*metav1.Condition) []*metav1.Condition { if len(messages) > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExpressionsValid, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonInvalidTransformsExpressions, Message: strings.Join(messages, "\n\n"), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExpressionsValid, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid", }) @@ -727,18 +727,18 @@ func appendTransformsExpressionsValidCondition(messages []string, conditions []* return conditions } -func appendTransformsExamplesPassedCondition(messages []string, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendTransformsExamplesPassedCondition(messages []string, conditions []*metav1.Condition) []*metav1.Condition { if len(messages) > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExamplesPassed, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonTransformsExamplesFailed, Message: strings.Join(messages, "\n\n"), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeTransformsExamplesPassed, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors", }) @@ -746,19 +746,19 @@ func appendTransformsExamplesPassedCondition(messages []string, conditions []*co return conditions } -func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames sets.Set[string], conditions []*metav1.Condition) []*metav1.Condition { if duplicateDisplayNames.Len() > 0 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersDisplayNamesUnique, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonDuplicateDisplayNames, Message: fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", strings.Join(sortAndQuote(duplicateDisplayNames.UnsortedList()), ", ")), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIdentityProvidersDisplayNamesUnique, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "the names specified by .spec.identityProviders[].displayName are unique", }) @@ -766,20 +766,20 @@ func appendIdentityProviderDuplicateDisplayNamesCondition(duplicateDisplayNames return conditions } -func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func appendIssuerURLValidCondition(err error, conditions []*metav1.Condition) []*metav1.Condition { if err != nil { // Note that the FederationDomainIssuer constructors only validate the Issuer URL, // so these are always issuer URL validation errors. - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonInvalidIssuerURL, Message: err.Error(), }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIssuerURLValid, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "spec.issuer is a valid URL", }) @@ -790,23 +790,23 @@ func appendIssuerURLValidCondition(err error, conditions []*configv1alpha1.Condi func (c *federationDomainWatcherController) updateStatus( ctx context.Context, federationDomain *configv1alpha1.FederationDomain, - conditions []*configv1alpha1.Condition, + conditions []*metav1.Condition, ) error { updated := federationDomain.DeepCopy() if hadErrorCondition(conditions) { updated.Status.Phase = configv1alpha1.FederationDomainPhaseError - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeReady, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonNotReady, Message: "the FederationDomain is not ready: see other conditions for details", }) } else { updated.Status.Phase = configv1alpha1.FederationDomainPhaseReady - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeReady, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: 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), @@ -858,20 +858,20 @@ func issuerURLToIssuerKey(issuerURL *url.URL) string { return fmt.Sprintf("%s://%s%s", issuerURL.Scheme, strings.ToLower(issuerURL.Host), issuerURL.Path) } -func (v *crossFederationDomainConfigValidator) Validate(federationDomain *configv1alpha1.FederationDomain, conditions []*configv1alpha1.Condition) []*configv1alpha1.Condition { +func (v *crossFederationDomainConfigValidator) Validate(federationDomain *configv1alpha1.FederationDomain, conditions []*metav1.Condition) []*metav1.Condition { issuerURL, urlParseErr := url.Parse(federationDomain.Spec.Issuer) if urlParseErr != nil { // Don't write a condition about the issuer URL being invalid because that is added elsewhere in the controller. - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIssuerIsUnique, - Status: configv1alpha1.ConditionUnknown, + Status: metav1.ConditionUnknown, Reason: reasonUnableToValidate, Message: "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed", }) - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeOneTLSSecretPerIssuerHostname, - Status: configv1alpha1.ConditionUnknown, + Status: metav1.ConditionUnknown, Reason: 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", }) @@ -879,32 +879,32 @@ func (v *crossFederationDomainConfigValidator) Validate(federationDomain *config } if issuerCount := v.issuerCounts[issuerURLToIssuerKey(issuerURL)]; issuerCount > 1 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIssuerIsUnique, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonDuplicateIssuer, Message: "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)", }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeIssuerIsUnique, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "spec.issuer is unique among all FederationDomains", }) } if len(v.uniqueSecretNamesPerIssuerAddress[issuerURLToHostnameKey(issuerURL)]) > 1 { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeOneTLSSecretPerIssuerHostname, - Status: configv1alpha1.ConditionFalse, + Status: metav1.ConditionFalse, Reason: reasonDifferentSecretRefsFound, Message: "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found", }) } else { - conditions = append(conditions, &configv1alpha1.Condition{ + conditions = append(conditions, &metav1.Condition{ Type: typeOneTLSSecretPerIssuerHostname, - Status: configv1alpha1.ConditionTrue, + Status: metav1.ConditionTrue, Reason: reasonSuccess, Message: "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL", }) @@ -950,9 +950,9 @@ func newCrossFederationDomainConfigValidator(federationDomains []*configv1alpha1 } } -func hadErrorCondition(conditions []*configv1alpha1.Condition) bool { +func hadErrorCondition(conditions []*metav1.Condition) bool { for _, c := range conditions { - if c.Status != configv1alpha1.ConditionTrue { + if c.Status != metav1.ConditionTrue { return true } } diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index 38c4cc54f..ba6055b58 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -194,8 +194,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { return fdIssuer } - happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "Ready", Status: "True", ObservedGeneration: observedGeneration, @@ -206,8 +206,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadReadyCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "Ready", Status: "False", ObservedGeneration: observedGeneration, @@ -217,8 +217,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerIsUnique", Status: "True", ObservedGeneration: observedGeneration, @@ -228,8 +228,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - unknownIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + unknownIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerIsUnique", Status: "Unknown", ObservedGeneration: observedGeneration, @@ -239,8 +239,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerIsUnique", Status: "False", ObservedGeneration: observedGeneration, @@ -250,8 +250,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "True", ObservedGeneration: observedGeneration, @@ -261,8 +261,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - unknownOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + unknownOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "Unknown", ObservedGeneration: observedGeneration, @@ -272,8 +272,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "OneTLSSecretPerIssuerHostname", Status: "False", ObservedGeneration: observedGeneration, @@ -283,8 +283,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyIssuerURLValidCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyIssuerURLValidCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerURLValid", Status: "True", ObservedGeneration: observedGeneration, @@ -294,8 +294,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIssuerURLValidConditionCannotHaveQuery := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIssuerURLValidConditionCannotHaveQuery := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, @@ -305,8 +305,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IssuerURLValid", Status: "False", ObservedGeneration: observedGeneration, @@ -316,8 +316,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersFound", Status: "True", ObservedGeneration: observedGeneration, @@ -330,8 +330,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyIdentityProvidersFoundConditionSuccess := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyIdentityProvidersFoundConditionSuccess := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersFound", Status: "True", ObservedGeneration: observedGeneration, @@ -341,8 +341,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, @@ -353,8 +353,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIdentityProvidersFoundConditionIdentityProviderNotSpecified := func(idpCRsCount int, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified := func(idpCRsCount int, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, @@ -367,8 +367,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersFound", Status: "False", ObservedGeneration: observedGeneration, @@ -378,8 +378,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersDisplayNamesUnique", Status: "True", ObservedGeneration: observedGeneration, @@ -389,8 +389,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersDisplayNamesUnique", Status: "False", ObservedGeneration: observedGeneration, @@ -400,8 +400,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "TransformsExpressionsValid", Status: "True", ObservedGeneration: observedGeneration, @@ -411,8 +411,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadTransformationExpressionsCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadTransformationExpressionsCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "TransformsExpressionsValid", Status: "False", ObservedGeneration: observedGeneration, @@ -422,8 +422,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "TransformsExamplesPassed", Status: "True", ObservedGeneration: observedGeneration, @@ -433,8 +433,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadTransformationExamplesCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadTransformationExamplesCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "TransformsExamplesPassed", Status: "False", ObservedGeneration: observedGeneration, @@ -444,8 +444,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "True", ObservedGeneration: observedGeneration, @@ -455,8 +455,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadAPIGroupSuffixCondition := func(badApiGroups string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadAPIGroupSuffixCondition := func(badApiGroups string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersObjectRefAPIGroupSuffixValid", Status: "False", ObservedGeneration: observedGeneration, @@ -467,8 +467,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - happyKindCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + happyKindCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersObjectRefKindValid", Status: "True", ObservedGeneration: observedGeneration, @@ -478,8 +478,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sadKindCondition := func(badKinds string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { - return configv1alpha1.Condition{ + sadKindCondition := func(badKinds string, time metav1.Time, observedGeneration int64) metav1.Condition { + return metav1.Condition{ Type: "IdentityProvidersObjectRefKindValid", Status: "False", ObservedGeneration: observedGeneration, @@ -490,8 +490,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { } } - sortConditionsByType := func(c []configv1alpha1.Condition) []configv1alpha1.Condition { - cp := make([]configv1alpha1.Condition, len(c)) + sortConditionsByType := func(c []metav1.Condition) []metav1.Condition { + cp := make([]metav1.Condition, len(c)) copy(cp, c) sort.SliceStable(cp, func(i, j int) bool { return cp[i].Type < cp[j].Type @@ -499,7 +499,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { return cp } - replaceConditions := func(conditions []configv1alpha1.Condition, sadConditions []configv1alpha1.Condition) []configv1alpha1.Condition { + replaceConditions := func(conditions []metav1.Condition, sadConditions []metav1.Condition) []metav1.Condition { for _, sadReplaceCondition := range sadConditions { for origIndex, origCondition := range conditions { if origCondition.Type == sadReplaceCondition.Type { @@ -511,8 +511,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { return conditions } - allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { - return sortConditionsByType([]configv1alpha1.Condition{ + allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []metav1.Condition { + return sortConditionsByType([]metav1.Condition{ happyTransformationExamplesCondition(frozenMetav1Now, 123), happyTransformationExpressionsCondition(frozenMetav1Now, 123), happyKindCondition(frozenMetav1Now, 123), @@ -526,10 +526,10 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }) } - allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { + allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []metav1.Condition { return replaceConditions( allHappyConditionsSuccess(issuer, time, observedGeneration), - []configv1alpha1.Condition{ + []metav1.Condition{ happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration), }, ) @@ -738,7 +738,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -780,7 +780,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -820,7 +820,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte.cOm/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -832,7 +832,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/a", oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -893,7 +893,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte-adDress.cOm/path1", oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -905,7 +905,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate-address.com:1234/path2", oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -917,7 +917,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(invalidIssuerURL, oidcIdentityProvider.Name, frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), @@ -945,7 +945,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -954,7 +954,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -975,7 +975,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1027,7 +1027,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "cant-find-me-name") @@ -1181,7 +1181,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1244,7 +1244,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( `cannot find resource specified by .spec.identityProviders[0].objectRef (with name "some-oidc-idp") @@ -1306,7 +1306,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( `cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp") @@ -1356,7 +1356,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadTransformationExpressionsCondition(here.Doc( `spec.identityProvider[0].transforms.expressions[0].expression was invalid: CEL expression compile error: ERROR: :1:6: Syntax error: mismatched input 'is' expecting @@ -1502,7 +1502,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadTransformationExamplesCondition(here.Doc( `.spec.identityProviders[0].transforms.examples[2] example failed: expected: authentication to be rejected @@ -1601,7 +1601,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadTransformationExamplesCondition(here.Doc( `.spec.identityProviders[0].transforms.examples[0] example failed: expected: no transformation errors @@ -1748,7 +1748,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123), sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123), sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc( @@ -1808,7 +1808,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { configv1alpha1.FederationDomainPhaseError, replaceConditions( allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123), - []configv1alpha1.Condition{ + []metav1.Condition{ sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadTransformationExpressionsCondition(here.Doc( `spec.identityProvider[0].transforms.expressions[1].expression was invalid: @@ -2135,7 +2135,7 @@ func convertToComparableType(fdis []*federationdomainproviders.FederationDomainI func expectedFederationDomainStatusUpdate( fd *configv1alpha1.FederationDomain, phase configv1alpha1.FederationDomainPhase, - conditions []configv1alpha1.Condition, + conditions []metav1.Condition, ) *configv1alpha1.FederationDomain { fdCopy := fd.DeepCopy() diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index b6a84b889..461ac7f49 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -658,26 +658,26 @@ func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name st require.NoError(t, err) } -func withAllSuccessfulConditions() map[string]v1alpha1.ConditionStatus { - return map[string]v1alpha1.ConditionStatus{ - "Ready": v1alpha1.ConditionTrue, - "IssuerIsUnique": v1alpha1.ConditionTrue, - "IdentityProvidersFound": v1alpha1.ConditionTrue, - "OneTLSSecretPerIssuerHostname": v1alpha1.ConditionTrue, - "IssuerURLValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefKindValid": v1alpha1.ConditionTrue, - "IdentityProvidersObjectRefAPIGroupSuffixValid": v1alpha1.ConditionTrue, - "IdentityProvidersDisplayNamesUnique": v1alpha1.ConditionTrue, - "TransformsExpressionsValid": v1alpha1.ConditionTrue, - "TransformsExamplesPassed": v1alpha1.ConditionTrue, +func withAllSuccessfulConditions() map[string]metav1.ConditionStatus { + return map[string]metav1.ConditionStatus{ + "Ready": metav1.ConditionTrue, + "IssuerIsUnique": metav1.ConditionTrue, + "IdentityProvidersFound": metav1.ConditionTrue, + "OneTLSSecretPerIssuerHostname": metav1.ConditionTrue, + "IssuerURLValid": metav1.ConditionTrue, + "IdentityProvidersObjectRefKindValid": metav1.ConditionTrue, + "IdentityProvidersObjectRefAPIGroupSuffixValid": metav1.ConditionTrue, + "IdentityProvidersDisplayNamesUnique": metav1.ConditionTrue, + "TransformsExpressionsValid": metav1.ConditionTrue, + "TransformsExamplesPassed": metav1.ConditionTrue, } } -func withFalseConditions(falseConditionTypes []string) map[string]v1alpha1.ConditionStatus { - c := map[string]v1alpha1.ConditionStatus{} +func withFalseConditions(falseConditionTypes []string) map[string]metav1.ConditionStatus { + c := map[string]metav1.ConditionStatus{} for k, v := range withAllSuccessfulConditions() { if slices.Contains(falseConditionTypes, k) { - c[k] = v1alpha1.ConditionFalse + c[k] = metav1.ConditionFalse } else { c[k] = v } @@ -685,7 +685,7 @@ func withFalseConditions(falseConditionTypes []string) map[string]v1alpha1.Condi return c } -func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, wantPhase v1alpha1.FederationDomainPhase, wantConditionTypeToStatus map[string]v1alpha1.ConditionStatus) { +func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, wantPhase v1alpha1.FederationDomainPhase, wantConditionTypeToStatus map[string]metav1.ConditionStatus) { t.Helper() testlib.RequireEventually(t, func(requireEventually *require.Assertions) { @@ -699,7 +699,7 @@ func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name st t.Logf("found FederationDomain %s/%s with phase %s, wanted phase %s", ns, name, actualPhase, wantPhase) requireEventually.Equalf(wantPhase, actualPhase, "unexpected phase (conditions = '%#v')", federationDomain.Status.Conditions) - actualConditionTypeToStatus := map[string]v1alpha1.ConditionStatus{} + actualConditionTypeToStatus := map[string]metav1.ConditionStatus{} for _, c := range federationDomain.Status.Conditions { actualConditionTypeToStatus[c.Type] = c.Status } diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index d28718a50..454d19de2 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -48,7 +48,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { }, v1alpha1.FederationDomainPhaseError) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulLegacyFederationDomainConditions("", fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "LegacyConfigurationIdentityProviderNotFound", Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource", @@ -77,7 +77,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulLegacyFederationDomainConditions(oidcIdentityProvider2.Name, fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProviderNotSpecified", Message: "no resources were specified by .spec.identityProviders[].objectRef and 2 identity provider " + @@ -123,7 +123,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { }, v1alpha1.FederationDomainPhaseError) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", Message: here.Docf(` @@ -147,7 +147,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", Message: fmt.Sprintf(`cannot find resource specified by .spec.identityProviders[1].objectRef (with name "%s")`, oidcIDP2Meta.Name), @@ -175,7 +175,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { testlib.WaitForFederationDomainStatusPhase(ctx, t, fd.Name, v1alpha1.FederationDomainPhaseError) testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", Message: fmt.Sprintf(`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "%s")`, oidcIDP1Meta.Name), @@ -342,7 +342,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersDisplayNamesUnique", Status: "False", Reason: "DuplicateDisplayNames", Message: `the names specified by .spec.identityProviders[].displayName contain duplicates: "not unique"`, @@ -485,7 +485,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { testlib.WaitForFederationDomainStatusConditions(ctx, t, fd.Name, replaceSomeConditions( allSuccessfulFederationDomainConditions(fd.Spec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "False", Reason: "IdentityProvidersObjectRefsNotFound", Message: `cannot find resource specified by .spec.identityProviders[2].objectRef (with name "also will not be found")`, @@ -950,8 +950,8 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) { } } -func replaceSomeConditions(conditions []v1alpha1.Condition, replaceWithTheseConditions []v1alpha1.Condition) []v1alpha1.Condition { - cp := make([]v1alpha1.Condition, len(conditions)) +func replaceSomeConditions(conditions []metav1.Condition, replaceWithTheseConditions []metav1.Condition) []metav1.Condition { + cp := make([]metav1.Condition, len(conditions)) copy(cp, conditions) for _, replacementCond := range replaceWithTheseConditions { for i, cond := range cp { @@ -964,10 +964,10 @@ func replaceSomeConditions(conditions []v1alpha1.Condition, replaceWithTheseCond return cp } -func allSuccessfulLegacyFederationDomainConditions(idpName string, federationDomainSpec v1alpha1.FederationDomainSpec) []v1alpha1.Condition { +func allSuccessfulLegacyFederationDomainConditions(idpName string, federationDomainSpec v1alpha1.FederationDomainSpec) []metav1.Condition { return replaceSomeConditions( allSuccessfulFederationDomainConditions(federationDomainSpec), - []v1alpha1.Condition{ + []metav1.Condition{ { Type: "IdentityProvidersFound", Status: "True", Reason: "LegacyConfigurationSuccess", Message: fmt.Sprintf(`no resources were specified by .spec.identityProviders[].objectRef but exactly one `+ @@ -979,8 +979,8 @@ func allSuccessfulLegacyFederationDomainConditions(idpName string, federationDom ) } -func allSuccessfulFederationDomainConditions(federationDomainSpec v1alpha1.FederationDomainSpec) []v1alpha1.Condition { - return []v1alpha1.Condition{ +func allSuccessfulFederationDomainConditions(federationDomainSpec v1alpha1.FederationDomainSpec) []metav1.Condition { + return []metav1.Condition{ { Type: "IdentityProvidersDisplayNamesUnique", Status: "True", Reason: "Success", Message: "the names specified by .spec.identityProviders[].displayName are unique", diff --git a/test/testlib/client.go b/test/testlib/client.go index c80c2e20d..f8e339708 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -329,7 +329,7 @@ func WaitForFederationDomainStatusPhase(ctx context.Context, t *testing.T, feder }, 60*time.Second, 1*time.Second, "expected the FederationDomain to have status %q", expectPhase) } -func WaitForFederationDomainStatusConditions(ctx context.Context, t *testing.T, federationDomainName string, expectConditions []configv1alpha1.Condition) { +func WaitForFederationDomainStatusConditions(ctx context.Context, t *testing.T, federationDomainName string, expectConditions []metav1.Condition) { t.Helper() testEnv := IntegrationEnv(t) federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace) From 8faf3b0e2665a56f82661806f7e7fcf3259c4683 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 11 Sep 2023 12:52:14 -0700 Subject: [PATCH 77/81] add workaround in update-codegen.sh for problem seen when run on linux --- hack/lib/update-codegen.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hack/lib/update-codegen.sh b/hack/lib/update-codegen.sh index fdf2a8d14..8be088473 100755 --- a/hack/lib/update-codegen.sh +++ b/hack/lib/update-codegen.sh @@ -40,6 +40,9 @@ if [[ "${#KUBE_VERSIONS[@]}" -ne 1 ]]; then exit 1 fi +# Add this to the git config inside the container to avoid permission errors when running this script on linux. +git config --global --add safe.directory /work + # Link the root directory into GOPATH since that is where output ends up. GOPATH_ROOT="${GOPATH}/src/${BASE_PKG}" mkdir -p "$(dirname "${GOPATH_ROOT}")" From 84498d5a55f026619b1c6eef909eddce2d6db2f2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 12 Sep 2023 09:34:19 -0700 Subject: [PATCH 78/81] fix imports grouping in manager.go --- internal/federationdomain/endpointsmanager/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/federationdomain/endpointsmanager/manager.go b/internal/federationdomain/endpointsmanager/manager.go index 494f8986c..fa96f3bbe 100644 --- a/internal/federationdomain/endpointsmanager/manager.go +++ b/internal/federationdomain/endpointsmanager/manager.go @@ -4,7 +4,6 @@ package endpointsmanager import ( - "go.pinniped.dev/internal/httputil/requestutil" "net/http" "strings" "sync" @@ -26,6 +25,7 @@ import ( "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/federationdomain/storage" + "go.pinniped.dev/internal/httputil/requestutil" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/secret" "go.pinniped.dev/pkg/oidcclient/nonce" From c52ed93bf8dcd26be5fefd7ba00abc95dfffdd84 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 12 Sep 2023 10:24:55 -0700 Subject: [PATCH 79/81] make prepare-supervisor-on-kind.sh work with older versions of bash --- hack/prepare-supervisor-on-kind.sh | 41 +++++++++++++----------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 1fdf4a814..91f9804aa 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -238,13 +238,23 @@ fi kubectl create secret tls -n "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" my-federation-domain-tls --cert "$tls_crt_path" --key "$tls_key_path" \ --dry-run=client --output yaml | kubectl apply -f - -# Variable that will be used to build up the "identityProviders" yaml for the FederationDomain. -fd_idps="" +# Make a FederationDomain using the TLS Secret and identity providers from above in a temp file. +fd_file="/tmp/federationdomain.yaml" +cat << EOF > $fd_file +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +kind: FederationDomain +metadata: + name: my-federation-domain +spec: + issuer: $issuer + tls: + secretName: my-federation-domain-tls + identityProviders: +EOF if [[ "$use_oidc_upstream" == "yes" ]]; then # Indenting the heredoc by 4 spaces to make it indented the correct amount in the FederationDomain below. - fd_idps="${fd_idps}$( - cat <> $fd_file - displayName: "My OIDC IDP 🚀" objectRef: @@ -264,13 +274,11 @@ if [[ "$use_oidc_upstream" == "yes" ]]; then username: oidc:ryan@example.com groups: [ oidc:a, oidc:b ] EOF - )" fi if [[ "$use_ldap_upstream" == "yes" ]]; then # Indenting the heredoc by 4 spaces to make it indented the correct amount in the FederationDomain below. - fd_idps="${fd_idps}$( - cat <> $fd_file - displayName: "My LDAP IDP 🚀" objectRef: @@ -320,13 +328,11 @@ if [[ "$use_ldap_upstream" == "yes" ]]; then rejected: true message: "Only users in certain kube groups are allowed to authenticate" EOF - )" fi if [[ "$use_ad_upstream" == "yes" ]]; then # Indenting the heredoc by 4 spaces to make it indented the correct amount in the FederationDomain below. - fd_idps="${fd_idps}$( - cat <> $fd_file - displayName: "My AD IDP" objectRef: @@ -334,21 +340,10 @@ if [[ "$use_ad_upstream" == "yes" ]]; then kind: ActiveDirectoryIdentityProvider name: my-ad-provider EOF - )" fi -# Make a FederationDomain using the TLS Secret and identity providers from above. -cat < Date: Wed, 13 Sep 2023 12:31:00 -0700 Subject: [PATCH 80/81] add celformer unit test demonstrating string regexp in CEL expressions --- internal/celtransformer/celformer_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index 10a148b1e..1e89d1131 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -234,6 +234,26 @@ func TestTransformer(t *testing.T) { wantUsername: "ryan", wantGroups: []string{"admins", "developers", "other"}, }, + { + name: "can use regex on strings: when the regex matches", + username: "ryan", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username.matches("^r[abcy].n$") ? "ryan-modified" : username`}, + }, + wantUsername: "ryan-modified", + wantGroups: []string{"admins", "developers", "other"}, + }, + { + name: "can use regex on strings: when the regex does not match", + username: "olive", + groups: []string{"admins", "developers", "other"}, + transforms: []CELTransformation{ + &UsernameTransformation{Expression: `username.matches("^r[abcy].n$") ? "ryan-modified" : username`}, + }, + wantUsername: "olive", + wantGroups: []string{"admins", "developers", "other"}, + }, { name: "can filter groups based on an allow list", username: "ryan", From 5573c629b59c72b5572382ed314cf16ac09bee08 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 13 Sep 2023 12:48:10 -0700 Subject: [PATCH 81/81] remove extra timeoutCtx for exec.CommandContext invocations in e2e test These extra timeout contexts were only in the new multiple IDPs e2e test. Remove this possible cause of test cleanup flakes where the test runs slow enough in CI that this timeout context has already expired and then the cleanup function fails with context deadline exceeded errors. --- test/integration/e2e_test.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index f684b5123..b145d46a7 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1390,10 +1390,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin for the LDAP IDP. t.Log("starting LDAP auth via kubectl") - timeoutCtx, cleanupTimeoutCtx := context.WithTimeout(testCtx, 10*time.Second) - t.Cleanup(cleanupTimeoutCtx) start := time.Now() - kubectlCmd := exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) + kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) ptyFile, err := pty.Start(kubectlCmd) require.NoError(t, err) @@ -1420,9 +1418,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Run "kubectl get namespaces" which should trigger a browser login via the plugin for the OIDC IDP. t.Log("starting OIDC auth via kubectl") - timeoutCtx, cleanupTimeoutCtx = context.WithTimeout(testCtx, 10*time.Second) - t.Cleanup(cleanupTimeoutCtx) - kubectlCmd = exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", oidcKubeconfigPath, "-v", "6") + kubectlCmd = exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", oidcKubeconfigPath, "-v", "6") kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. @@ -1507,9 +1503,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin for the LDAP IDP. t.Log("starting second LDAP auth via kubectl") - timeoutCtx, cleanupTimeoutCtx = context.WithTimeout(testCtx, 10*time.Second) - t.Cleanup(cleanupTimeoutCtx) - kubectlCmd = exec.CommandContext(timeoutCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) + kubectlCmd = exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", ldapKubeconfigPath) kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) ptyFile, err = pty.Start(kubectlCmd) require.NoError(t, err)