Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple IDPs and identity transformations on Supervisor FederationDomains #1419

Merged
merged 81 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
be11966
Add APIs for multiple IDP and id transformations to FederationDomain CRD
cfryanr Feb 15, 2023
5385fb3
Add identity transformation packages idtransform and celformer
cfryanr Feb 6, 2023
1a53b4d
Allow user-defined string & stringList consts for use in CEL expressions
cfryanr Feb 7, 2023
7af75df
First draft of implementation of multiple IDPs support
cfryanr May 8, 2023
32aa015
Fixup unit tests for the previous commit
cfryanr Jun 5, 2023
b762720
Add tests for identity_transformation.go
benjaminapetersen Jun 7, 2023
9609884
Get tests to compile again and fix lint errors
cfryanr Jun 13, 2023
5c0425f
refactor: rename "provider" to "federationdomain" when appropriate
benjaminapetersen Jun 13, 2023
8f6a12e
fix internal/oidc/provider/manager/manager_test.go
benjaminapetersen Jun 14, 2023
793d1c6
add a type assertion
cfryanr Jun 15, 2023
6ef9cf2
Fix post_login_handler_test.go
benjaminapetersen Jun 15, 2023
770f8af
Update auth_handler.go to return 422 error when upstream IdP not found
benjaminapetersen Jun 15, 2023
610f886
Fix auth_handler_test.go
cfryanr Jun 16, 2023
3160b5b
Reorganized FederationDomain packages to avoid circular dependency
benjaminapetersen Jun 22, 2023
86c791b
reorganize federation domain packages to be more intuitive
cfryanr Jun 22, 2023
9d79235
test FederationDomainIdentityProvidersListerFinder
benjaminapetersen Jun 26, 2023
b71e596
fix token_handler_test.go
cfryanr Jun 26, 2023
048f05d
fix callback_handler_test.go
cfryanr Jun 26, 2023
98ee9f0
escape semicolons in variable values in integration-test-env-goland.sh
cfryanr Jun 26, 2023
0f23931
Fix some tests in supervisor_login_test.go
cfryanr Jun 26, 2023
2c4927d
update unit test that fails on slow CI workers
cfryanr Jun 26, 2023
514f996
update 1.27 codegen for multiple IDPs
cfryanr Jun 26, 2023
e4f4368
fix more integration tests for multiple IDPs
cfryanr Jun 27, 2023
022fdb9
Update a test assertion to make failure easier to understand
cfryanr Jun 28, 2023
0b408f4
Change FederationDomain.Status to use Phase and Conditions
cfryanr Jun 30, 2023
3521e12
Change name of FederationDomain printer column back to "Status"
cfryanr Jul 5, 2023
5e2f98a
Update informers unit test for FederationDomainWatcherController
cfryanr Jul 5, 2023
48e44e1
Change federation_domain_watcher_test.go to use a test table style
cfryanr Jul 6, 2023
e9fb424
Update federation_domain_watcher with new IdentityProviderFound
benjaminapetersen Jul 7, 2023
fe9364c
Expand IdentityProvidersFound condition in federation_domain_watcher
benjaminapetersen Jul 10, 2023
97a374c
Refactor federation_domain_watcher_test.go and add new test to its table
cfryanr Jul 10, 2023
40dcc8a
Update integration tests for new FederationDomain phase behavior
cfryanr Jul 11, 2023
e334ad6
Fix lint errors in federation_domain_watcher.go, and adjust unit test
cfryanr Jul 11, 2023
a38fb16
Load FederationDomain endpoints before updating its status
cfryanr Jul 11, 2023
7670989
Refactor: extract helper functions in federation_domain_watcher.go
cfryanr Jul 11, 2023
a9f2f67
Handle some unexpected errors in federation_domain_watcher.go
cfryanr Jul 11, 2023
31d67a1
Validate display names are unique in federation_domain_watcher.go
cfryanr Jul 12, 2023
32063db
Validate apiGroup names are valid in federation_domain_watcher.go
cfryanr Jul 12, 2023
8e169f9
Validate IDP objectRef kind names in federation_domain_watcher.go
cfryanr Jul 12, 2023
b05e8a5
Replace sleep with kubectl wait in prepare-supervisor-on-kind.sh
cfryanr Jul 12, 2023
0aacedf
Update proposal doc statuses
cfryanr Jul 13, 2023
617f57e
Validate transforms const names in federation_domain_watcher.go
cfryanr Jul 13, 2023
be973bc
Allow for slower CI workers in celformer_test.go
cfryanr Jul 13, 2023
0130300
Add helper for happy/sad conditions to federation_domain_watcher_test.go
benjaminapetersen Jul 14, 2023
52925a2
Validate transforms expressions in federation_domain_watcher.go
cfryanr Jul 14, 2023
c771328
Validate transforms examples in federation_domain_watcher.go
cfryanr Jul 14, 2023
b89e6d9
Make it possible to compare transformation pipelines in unit tests
cfryanr Jul 17, 2023
e42e3ca
Status condition messages for IDP transforms show index of invalid IDP
cfryanr Jul 18, 2023
64f41d0
use multiple IDPs in manager_test.go
cfryanr Jul 18, 2023
61bb01b
extract a helper function in federation_domain_watcher.go
cfryanr Jul 19, 2023
4b75ced
add unit tests for getters in federation_domain_issuer_test.go
cfryanr Jul 19, 2023
84041e0
add unit test for ApplyIdentityTransformations helper
cfryanr Jul 19, 2023
23ed285
small refactor in supervisor_discovery_test.go
cfryanr Jul 19, 2023
5341322
add integration test for FederationDomain status updates
cfryanr Jul 20, 2023
5174236
wordsmith some FederationDomain status messages
cfryanr Jul 20, 2023
bd5cabf
fix some here.Doc string indents in federation_domain_watcher_test.go
cfryanr Jul 20, 2023
6d82a11
CRD already validates that IDP transform constant names are unique
cfryanr Jul 21, 2023
446384a
add an e2e test for a FederationDomain with multiple IDPs and transforms
cfryanr Jul 21, 2023
92bf826
rename a local variable in an integration test
cfryanr Jul 21, 2023
c701a4a
remove expectation about TransformsConstantsNamesUnique status condition
cfryanr Jul 21, 2023
957892b
handle old versions of k8s in supervisor_federationdomain_status_test.go
cfryanr Jul 25, 2023
01ab775
Add e2e test for rejecting auth using identity transformation policy
cfryanr Jul 25, 2023
e6c78fa
Fix expectations in FederationDomains status test for old Kube versions
cfryanr Jul 26, 2023
519aece
Start adding identity transformations tests to supervisor_login_test.go
cfryanr Jul 28, 2023
0a21cb6
Replace more pointer.String() with the new ptr.To()
cfryanr Jul 28, 2023
2eb82cc
Add more tests with identity transformations in supervisor_login_test.go
cfryanr Jul 28, 2023
b2656b9
add new unit tests in auth_handler_test.go
cfryanr Aug 23, 2023
d4611b8
use slices.Contains() instead of custom func in token_handler_test.go
cfryanr Aug 23, 2023
f653942
add new unit tests in callback_handler_test.go
cfryanr Aug 23, 2023
7f70fcf
add units tests to post_login_handler_test.go
cfryanr Aug 23, 2023
5ad7e9a
started add units tests for identity transforms to token_handler_test.go
cfryanr Aug 23, 2023
593d55e
run codegen again after rebasing main branch into feature branch
cfryanr Aug 29, 2023
28210ab
add units tests to token_handler_test.go
cfryanr Aug 29, 2023
e2bdab9
add the IDP display name to the downstream ID token's `sub` claim
cfryanr Aug 30, 2023
b6f0dc3
Fix conflicts caused from rebasing main into multiple IDPs branch
cfryanr Sep 7, 2023
a7bd494
update FederationDomain.status.conditions to come from metav1
cfryanr Sep 11, 2023
8faf3b0
add workaround in update-codegen.sh for problem seen when run on linux
cfryanr Sep 11, 2023
84498d5
fix imports grouping in manager.go
cfryanr Sep 12, 2023
c52ed93
make prepare-supervisor-on-kind.sh work with older versions of bash
cfryanr Sep 12, 2023
2cecc17
add celformer unit test demonstrating string regexp in CEL expressions
cfryanr Sep 13, 2023
5573c62
remove extra timeoutCtx for exec.CommandContext invocations in e2e test
cfryanr Sep 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 199 additions & 22 deletions apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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.
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
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"`
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
}

// FederationDomainTransformsExample defines a transform example.
type FederationDomainTransformsExample struct {
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
// 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"`
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
// 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
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
// 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"`
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
}

// 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"`
cfryanr marked this conversation as resolved.
Show resolved Hide resolved

// 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
Expand All @@ -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"`

cfryanr marked this conversation as resolved.
Show resolved Hide resolved
// 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"`
cfryanr marked this conversation as resolved.
Show resolved Hide resolved
}

// FederationDomainSecrets holds information about this OIDC Provider's secrets.
Expand Down Expand Up @@ -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 []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`

// Secrets contains information about this OIDC Provider's secrets.
// +optional
Expand All @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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+)?/`
Expand Down
2 changes: 1 addition & 1 deletion cmd/pinniped/cmd/login_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
Loading