wiring existing oidc flags with internal API struct
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
		| @@ -19,11 +19,11 @@ package authenticator | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	utilnet "k8s.io/apimachinery/pkg/util/net" | 	utilnet "k8s.io/apimachinery/pkg/util/net" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/group" | 	"k8s.io/apiserver/pkg/authentication/group" | ||||||
| @@ -55,15 +55,8 @@ type Config struct { | |||||||
| 	BootstrapToken bool | 	BootstrapToken bool | ||||||
|  |  | ||||||
| 	TokenAuthFile               string | 	TokenAuthFile               string | ||||||
| 	OIDCIssuerURL               string | 	AuthenticationConfig        *apiserver.AuthenticationConfiguration | ||||||
| 	OIDCClientID                string |  | ||||||
| 	OIDCCAFile                  string |  | ||||||
| 	OIDCUsernameClaim           string |  | ||||||
| 	OIDCUsernamePrefix          string |  | ||||||
| 	OIDCGroupsClaim             string |  | ||||||
| 	OIDCGroupsPrefix            string |  | ||||||
| 	OIDCSigningAlgs             []string | 	OIDCSigningAlgs             []string | ||||||
| 	OIDCRequiredClaims          map[string]string |  | ||||||
| 	ServiceAccountKeyFiles      []string | 	ServiceAccountKeyFiles      []string | ||||||
| 	ServiceAccountLookup        bool | 	ServiceAccountLookup        bool | ||||||
| 	ServiceAccountIssuers       []string | 	ServiceAccountIssuers       []string | ||||||
| @@ -153,33 +146,28 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er | |||||||
| 	// cache misses for all requests using the other. While the service account plugin | 	// cache misses for all requests using the other. While the service account plugin | ||||||
| 	// simply returns an error, the OpenID Connect plugin may query the provider to | 	// simply returns an error, the OpenID Connect plugin may query the provider to | ||||||
| 	// update the keys, causing performance hits. | 	// update the keys, causing performance hits. | ||||||
| 	if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 { | 	if config.AuthenticationConfig != nil { | ||||||
| 		// TODO(enj): wire up the Notifier and ControllerRunner bits when OIDC supports CA reload | 		for _, jwtAuthenticator := range config.AuthenticationConfig.JWT { | ||||||
| 		var oidcCAContent oidc.CAContentProvider | 			var oidcCAContent oidc.CAContentProvider | ||||||
| 		if len(config.OIDCCAFile) != 0 { | 			if len(jwtAuthenticator.Issuer.CertificateAuthority) > 0 { | ||||||
| 			var oidcCAErr error | 				var oidcCAError error | ||||||
| 			oidcCAContent, oidcCAErr = staticCAContentProviderFromFile("oidc-authenticator", config.OIDCCAFile) | 				oidcCAContent, oidcCAError = dynamiccertificates.NewStaticCAContent("oidc-authenticator", []byte(jwtAuthenticator.Issuer.CertificateAuthority)) | ||||||
| 			if oidcCAErr != nil { | 				if oidcCAError != nil { | ||||||
| 				return nil, nil, oidcCAErr | 					return nil, nil, oidcCAError | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
|  | 			oidcAuth, err := oidc.New(oidc.Options{ | ||||||
|  | 				JWTAuthenticator:     jwtAuthenticator, | ||||||
|  | 				CAContentProvider:    oidcCAContent, | ||||||
|  | 				SupportedSigningAlgs: config.OIDCSigningAlgs, | ||||||
|  | 			}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, nil, err | ||||||
|  | 			} | ||||||
|  | 			tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, oidcAuth)) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(oidc.Options{ |  | ||||||
| 			IssuerURL:            config.OIDCIssuerURL, |  | ||||||
| 			ClientID:             config.OIDCClientID, |  | ||||||
| 			CAContentProvider:    oidcCAContent, |  | ||||||
| 			UsernameClaim:        config.OIDCUsernameClaim, |  | ||||||
| 			UsernamePrefix:       config.OIDCUsernamePrefix, |  | ||||||
| 			GroupsClaim:          config.OIDCGroupsClaim, |  | ||||||
| 			GroupsPrefix:         config.OIDCGroupsPrefix, |  | ||||||
| 			SupportedSigningAlgs: config.OIDCSigningAlgs, |  | ||||||
| 			RequiredClaims:       config.OIDCRequiredClaims, |  | ||||||
| 		}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, nil, err |  | ||||||
| 		} |  | ||||||
| 		tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, oidcAuth)) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(config.WebhookTokenAuthnConfigFile) > 0 { | 	if len(config.WebhookTokenAuthnConfigFile) > 0 { | ||||||
| 		webhookTokenAuth, err := newWebhookTokenAuthenticator(config) | 		webhookTokenAuth, err := newWebhookTokenAuthenticator(config) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -243,31 +231,6 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e | |||||||
| 	return tokenAuthenticator, nil | 	return tokenAuthenticator, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error. |  | ||||||
| func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token, error) { |  | ||||||
| 	const noUsernamePrefix = "-" |  | ||||||
|  |  | ||||||
| 	if opts.UsernamePrefix == "" && opts.UsernameClaim != "email" { |  | ||||||
| 		// Old behavior. If a usernamePrefix isn't provided, prefix all claims other than "email" |  | ||||||
| 		// with the issuerURL. |  | ||||||
| 		// |  | ||||||
| 		// See https://github.com/kubernetes/kubernetes/issues/31380 |  | ||||||
| 		opts.UsernamePrefix = opts.IssuerURL + "#" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if opts.UsernamePrefix == noUsernamePrefix { |  | ||||||
| 		// Special value indicating usernames shouldn't be prefixed. |  | ||||||
| 		opts.UsernamePrefix = "" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	tokenAuthenticator, err := oidc.New(opts) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return tokenAuthenticator, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error | // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error | ||||||
| func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) { | func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) { | ||||||
| 	allPublicKeys := []interface{}{} | 	allPublicKeys := []interface{}{} | ||||||
| @@ -318,12 +281,3 @@ func newWebhookTokenAuthenticator(config Config) (authenticator.Token, error) { | |||||||
|  |  | ||||||
| 	return tokencache.New(webhookTokenAuthenticator, false, config.WebhookTokenAuthnCacheTTL, config.WebhookTokenAuthnCacheTTL), nil | 	return tokencache.New(webhookTokenAuthenticator, false, config.WebhookTokenAuthnCacheTTL, config.WebhookTokenAuthnCacheTTL), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func staticCAContentProviderFromFile(purpose, filename string) (dynamiccertificates.CAContentProvider, error) { |  | ||||||
| 	fileBytes, err := os.ReadFile(filename) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return dynamiccertificates.NewStaticCAContent(purpose, fileBytes) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -28,6 +29,8 @@ import ( | |||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||||
|  | 	apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||||
| 	genericapiserver "k8s.io/apiserver/pkg/server" | 	genericapiserver "k8s.io/apiserver/pkg/server" | ||||||
| 	"k8s.io/apiserver/pkg/server/egressselector" | 	"k8s.io/apiserver/pkg/server/egressselector" | ||||||
| @@ -41,6 +44,7 @@ import ( | |||||||
| 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | ||||||
| 	authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" | 	authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" | ||||||
| 	"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap" | 	"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap" | ||||||
|  | 	"k8s.io/utils/pointer" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // BuiltInAuthenticationOptions contains all build-in authentication options for API Server | // BuiltInAuthenticationOptions contains all build-in authentication options for API Server | ||||||
| @@ -397,16 +401,68 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if o.OIDC != nil { | 	if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { | ||||||
| 		ret.OIDCCAFile = o.OIDC.CAFile | 		usernamePrefix := o.OIDC.UsernamePrefix | ||||||
| 		ret.OIDCClientID = o.OIDC.ClientID |  | ||||||
| 		ret.OIDCGroupsClaim = o.OIDC.GroupsClaim | 		if o.OIDC.UsernamePrefix == "" && o.OIDC.UsernameClaim != "email" { | ||||||
| 		ret.OIDCGroupsPrefix = o.OIDC.GroupsPrefix | 			// Legacy CLI flag behavior. If a usernamePrefix isn't provided, prefix all claims other than "email" | ||||||
| 		ret.OIDCIssuerURL = o.OIDC.IssuerURL | 			// with the issuerURL. | ||||||
| 		ret.OIDCUsernameClaim = o.OIDC.UsernameClaim | 			// | ||||||
| 		ret.OIDCUsernamePrefix = o.OIDC.UsernamePrefix | 			// See https://github.com/kubernetes/kubernetes/issues/31380 | ||||||
|  | 			usernamePrefix = o.OIDC.IssuerURL + "#" | ||||||
|  | 		} | ||||||
|  | 		if o.OIDC.UsernamePrefix == "-" { | ||||||
|  | 			// Special value indicating usernames shouldn't be prefixed. | ||||||
|  | 			usernamePrefix = "" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		jwtAuthenticator := apiserver.JWTAuthenticator{ | ||||||
|  | 			Issuer: apiserver.Issuer{ | ||||||
|  | 				URL:       o.OIDC.IssuerURL, | ||||||
|  | 				Audiences: []string{o.OIDC.ClientID}, | ||||||
|  | 			}, | ||||||
|  | 			ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 				Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 					Prefix: pointer.String(usernamePrefix), | ||||||
|  | 					Claim:  o.OIDC.UsernameClaim, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(o.OIDC.GroupsClaim) > 0 { | ||||||
|  | 			jwtAuthenticator.ClaimMappings.Groups = apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 				Prefix: pointer.String(o.OIDC.GroupsPrefix), | ||||||
|  | 				Claim:  o.OIDC.GroupsClaim, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(o.OIDC.CAFile) != 0 { | ||||||
|  | 			caContent, err := os.ReadFile(o.OIDC.CAFile) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return kubeauthenticator.Config{}, err | ||||||
|  | 			} | ||||||
|  | 			jwtAuthenticator.Issuer.CertificateAuthority = string(caContent) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(o.OIDC.RequiredClaims) > 0 { | ||||||
|  | 			claimValidationRules := make([]apiserver.ClaimValidationRule, 0, len(o.OIDC.RequiredClaims)) | ||||||
|  | 			for claim, value := range o.OIDC.RequiredClaims { | ||||||
|  | 				claimValidationRules = append(claimValidationRules, apiserver.ClaimValidationRule{ | ||||||
|  | 					Claim:         claim, | ||||||
|  | 					RequiredValue: value, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 			jwtAuthenticator.ClaimValidationRules = claimValidationRules | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		authConfig := &apiserver.AuthenticationConfiguration{ | ||||||
|  | 			JWT: []apiserver.JWTAuthenticator{jwtAuthenticator}, | ||||||
|  | 		} | ||||||
|  | 		if err := apiservervalidation.ValidateAuthenticationConfiguration(authConfig).ToAggregate(); err != nil { | ||||||
|  | 			return kubeauthenticator.Config{}, err | ||||||
|  | 		} | ||||||
|  | 		ret.AuthenticationConfig = authConfig | ||||||
| 		ret.OIDCSigningAlgs = o.OIDC.SigningAlgs | 		ret.OIDCSigningAlgs = o.OIDC.SigningAlgs | ||||||
| 		ret.OIDCRequiredClaims = o.OIDC.RequiredClaims |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if o.RequestHeader != nil { | 	if o.RequestHeader != nil { | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ limitations under the License. | |||||||
| package options | package options | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"os" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| @@ -27,11 +28,13 @@ import ( | |||||||
|  |  | ||||||
| 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/request/headerrequest" | 	"k8s.io/apiserver/pkg/authentication/request/headerrequest" | ||||||
| 	apiserveroptions "k8s.io/apiserver/pkg/server/options" | 	apiserveroptions "k8s.io/apiserver/pkg/server/options" | ||||||
| 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | ||||||
|  | 	"k8s.io/utils/pointer" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAuthenticationValidate(t *testing.T) { | func TestAuthenticationValidate(t *testing.T) { | ||||||
| @@ -50,7 +53,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -63,7 +66,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| 				Issuers:  []string{"http://foo.bar.com"}, | 				Issuers:  []string{"http://foo.bar.com"}, | ||||||
| @@ -76,7 +79,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -89,7 +92,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -102,7 +105,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -115,7 +118,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -128,7 +131,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -141,7 +144,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -156,7 +159,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -171,7 +174,7 @@ func TestAuthenticationValidate(t *testing.T) { | |||||||
| 			testOIDC: &OIDCAuthenticationOptions{ | 			testOIDC: &OIDCAuthenticationOptions{ | ||||||
| 				UsernameClaim: "sub", | 				UsernameClaim: "sub", | ||||||
| 				SigningAlgs:   []string{"RS256"}, | 				SigningAlgs:   []string{"RS256"}, | ||||||
| 				IssuerURL:     "testIssuerURL", | 				IssuerURL:     "https://testIssuerURL", | ||||||
| 				ClientID:      "testClientID", | 				ClientID:      "testClientID", | ||||||
| 			}, | 			}, | ||||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | 			testSA: &ServiceAccountAuthenticationOptions{ | ||||||
| @@ -228,10 +231,10 @@ func TestToAuthenticationConfig(t *testing.T) { | |||||||
| 			Enable: false, | 			Enable: false, | ||||||
| 		}, | 		}, | ||||||
| 		OIDC: &OIDCAuthenticationOptions{ | 		OIDC: &OIDCAuthenticationOptions{ | ||||||
| 			CAFile:        "/testCAFile", | 			CAFile:        "testdata/root.pem", | ||||||
| 			UsernameClaim: "sub", | 			UsernameClaim: "sub", | ||||||
| 			SigningAlgs:   []string{"RS256"}, | 			SigningAlgs:   []string{"RS256"}, | ||||||
| 			IssuerURL:     "testIssuerURL", | 			IssuerURL:     "https://testIssuerURL", | ||||||
| 			ClientID:      "testClientID", | 			ClientID:      "testClientID", | ||||||
| 		}, | 		}, | ||||||
| 		RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{ | 		RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{ | ||||||
| @@ -253,15 +256,27 @@ func TestToAuthenticationConfig(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	expectConfig := kubeauthenticator.Config{ | 	expectConfig := kubeauthenticator.Config{ | ||||||
| 		APIAudiences:                authenticator.Audiences{"http://foo.bar.com"}, | 		APIAudiences:            authenticator.Audiences{"http://foo.bar.com"}, | ||||||
| 		Anonymous:                   false, | 		Anonymous:               false, | ||||||
| 		BootstrapToken:              false, | 		BootstrapToken:          false, | ||||||
| 		ClientCAContentProvider:     nil, // this is nil because you can't compare functions | 		ClientCAContentProvider: nil, // this is nil because you can't compare functions | ||||||
| 		TokenAuthFile:               "/testTokenFile", | 		TokenAuthFile:           "/testTokenFile", | ||||||
| 		OIDCIssuerURL:               "testIssuerURL", | 		AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
| 		OIDCClientID:                "testClientID", | 			JWT: []apiserver.JWTAuthenticator{ | ||||||
| 		OIDCCAFile:                  "/testCAFile", | 				{ | ||||||
| 		OIDCUsernameClaim:           "sub", | 					Issuer: apiserver.Issuer{ | ||||||
|  | 						URL:       "https://testIssuerURL", | ||||||
|  | 						Audiences: []string{"testClientID"}, | ||||||
|  | 					}, | ||||||
|  | 					ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 						Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 							Claim:  "sub", | ||||||
|  | 							Prefix: pointer.String("https://testIssuerURL#"), | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		OIDCSigningAlgs:             []string{"RS256"}, | 		OIDCSigningAlgs:             []string{"RS256"}, | ||||||
| 		ServiceAccountLookup:        true, | 		ServiceAccountLookup:        true, | ||||||
| 		ServiceAccountIssuers:       []string{"http://foo.bar.com"}, | 		ServiceAccountIssuers:       []string{"http://foo.bar.com"}, | ||||||
| @@ -280,6 +295,12 @@ func TestToAuthenticationConfig(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	fileBytes, err := os.ReadFile("testdata/root.pem") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	expectConfig.AuthenticationConfig.JWT[0].Issuer.CertificateAuthority = string(fileBytes) | ||||||
|  |  | ||||||
| 	resultConfig, err := testOptions.ToAuthenticationConfig() | 	resultConfig, err := testOptions.ToAuthenticationConfig() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| @@ -385,3 +406,221 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { | |||||||
| 		t.Error(cmp.Diff(opts, expected)) | 		t.Error(cmp.Diff(opts, expected)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestToAuthenticationConfig_OIDC(t *testing.T) { | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name         string | ||||||
|  | 		args         []string | ||||||
|  | 		expectConfig kubeauthenticator.Config | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "username prefix is '-'", | ||||||
|  | 			args: []string{ | ||||||
|  | 				"--oidc-issuer-url=https://testIssuerURL", | ||||||
|  | 				"--oidc-client-id=testClientID", | ||||||
|  | 				"--oidc-username-claim=sub", | ||||||
|  | 				"--oidc-username-prefix=-", | ||||||
|  | 				"--oidc-signing-algs=RS256", | ||||||
|  | 				"--oidc-required-claim=foo=bar", | ||||||
|  | 			}, | ||||||
|  | 			expectConfig: kubeauthenticator.Config{ | ||||||
|  | 				TokenSuccessCacheTTL: 10 * time.Second, | ||||||
|  | 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
|  | 					JWT: []apiserver.JWTAuthenticator{ | ||||||
|  | 						{ | ||||||
|  | 							Issuer: apiserver.Issuer{ | ||||||
|  | 								URL:       "https://testIssuerURL", | ||||||
|  | 								Audiences: []string{"testClientID"}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 								Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "sub", | ||||||
|  | 									Prefix: pointer.String(""), | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||||
|  | 								{ | ||||||
|  | 									Claim:         "foo", | ||||||
|  | 									RequiredValue: "bar", | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				OIDCSigningAlgs: []string{"RS256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "--oidc-username-prefix is empty, --oidc-username-claim is not email", | ||||||
|  | 			args: []string{ | ||||||
|  | 				"--oidc-issuer-url=https://testIssuerURL", | ||||||
|  | 				"--oidc-client-id=testClientID", | ||||||
|  | 				"--oidc-username-claim=sub", | ||||||
|  | 				"--oidc-signing-algs=RS256", | ||||||
|  | 				"--oidc-required-claim=foo=bar", | ||||||
|  | 			}, | ||||||
|  | 			expectConfig: kubeauthenticator.Config{ | ||||||
|  | 				TokenSuccessCacheTTL: 10 * time.Second, | ||||||
|  | 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
|  | 					JWT: []apiserver.JWTAuthenticator{ | ||||||
|  | 						{ | ||||||
|  | 							Issuer: apiserver.Issuer{ | ||||||
|  | 								URL:       "https://testIssuerURL", | ||||||
|  | 								Audiences: []string{"testClientID"}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 								Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "sub", | ||||||
|  | 									Prefix: pointer.String("https://testIssuerURL#"), | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||||
|  | 								{ | ||||||
|  | 									Claim:         "foo", | ||||||
|  | 									RequiredValue: "bar", | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				OIDCSigningAlgs: []string{"RS256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "--oidc-username-prefix is empty, --oidc-username-claim is email", | ||||||
|  | 			args: []string{ | ||||||
|  | 				"--oidc-issuer-url=https://testIssuerURL", | ||||||
|  | 				"--oidc-client-id=testClientID", | ||||||
|  | 				"--oidc-username-claim=email", | ||||||
|  | 				"--oidc-signing-algs=RS256", | ||||||
|  | 				"--oidc-required-claim=foo=bar", | ||||||
|  | 			}, | ||||||
|  | 			expectConfig: kubeauthenticator.Config{ | ||||||
|  | 				TokenSuccessCacheTTL: 10 * time.Second, | ||||||
|  | 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
|  | 					JWT: []apiserver.JWTAuthenticator{ | ||||||
|  | 						{ | ||||||
|  | 							Issuer: apiserver.Issuer{ | ||||||
|  | 								URL:       "https://testIssuerURL", | ||||||
|  | 								Audiences: []string{"testClientID"}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 								Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "email", | ||||||
|  | 									Prefix: pointer.String(""), | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||||
|  | 								{ | ||||||
|  | 									Claim:         "foo", | ||||||
|  | 									RequiredValue: "bar", | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				OIDCSigningAlgs: []string{"RS256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "non empty username prefix", | ||||||
|  | 			args: []string{ | ||||||
|  | 				"--oidc-issuer-url=https://testIssuerURL", | ||||||
|  | 				"--oidc-client-id=testClientID", | ||||||
|  | 				"--oidc-username-claim=sub", | ||||||
|  | 				"--oidc-username-prefix=k8s-", | ||||||
|  | 				"--oidc-signing-algs=RS256", | ||||||
|  | 				"--oidc-required-claim=foo=bar", | ||||||
|  | 			}, | ||||||
|  | 			expectConfig: kubeauthenticator.Config{ | ||||||
|  | 				TokenSuccessCacheTTL: 10 * time.Second, | ||||||
|  | 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
|  | 					JWT: []apiserver.JWTAuthenticator{ | ||||||
|  | 						{ | ||||||
|  | 							Issuer: apiserver.Issuer{ | ||||||
|  | 								URL:       "https://testIssuerURL", | ||||||
|  | 								Audiences: []string{"testClientID"}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 								Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "sub", | ||||||
|  | 									Prefix: pointer.String("k8s-"), | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||||
|  | 								{ | ||||||
|  | 									Claim:         "foo", | ||||||
|  | 									RequiredValue: "bar", | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				OIDCSigningAlgs: []string{"RS256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "groups claim exists", | ||||||
|  | 			args: []string{ | ||||||
|  | 				"--oidc-issuer-url=https://testIssuerURL", | ||||||
|  | 				"--oidc-client-id=testClientID", | ||||||
|  | 				"--oidc-username-claim=sub", | ||||||
|  | 				"--oidc-username-prefix=-", | ||||||
|  | 				"--oidc-groups-claim=groups", | ||||||
|  | 				"--oidc-groups-prefix=oidc:", | ||||||
|  | 				"--oidc-signing-algs=RS256", | ||||||
|  | 				"--oidc-required-claim=foo=bar", | ||||||
|  | 			}, | ||||||
|  | 			expectConfig: kubeauthenticator.Config{ | ||||||
|  | 				TokenSuccessCacheTTL: 10 * time.Second, | ||||||
|  | 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||||
|  | 					JWT: []apiserver.JWTAuthenticator{ | ||||||
|  | 						{ | ||||||
|  | 							Issuer: apiserver.Issuer{ | ||||||
|  | 								URL:       "https://testIssuerURL", | ||||||
|  | 								Audiences: []string{"testClientID"}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimMappings: apiserver.ClaimMappings{ | ||||||
|  | 								Username: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "sub", | ||||||
|  | 									Prefix: pointer.String(""), | ||||||
|  | 								}, | ||||||
|  | 								Groups: apiserver.PrefixedClaimOrExpression{ | ||||||
|  | 									Claim:  "groups", | ||||||
|  | 									Prefix: pointer.String("oidc:"), | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||||
|  | 								{ | ||||||
|  | 									Claim:         "foo", | ||||||
|  | 									RequiredValue: "bar", | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				OIDCSigningAlgs: []string{"RS256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, testcase := range testCases { | ||||||
|  | 		t.Run(testcase.name, func(t *testing.T) { | ||||||
|  | 			opts := NewBuiltInAuthenticationOptions().WithOIDC() | ||||||
|  | 			pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError) | ||||||
|  | 			opts.AddFlags(pf) | ||||||
|  |  | ||||||
|  | 			if err := pf.Parse(testcase.args); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			resultConfig, err := opts.ToAuthenticationConfig() | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if !reflect.DeepEqual(resultConfig, testcase.expectConfig) { | ||||||
|  | 				t.Error(cmp.Diff(resultConfig, testcase.expectConfig)) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,204 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2023 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package validation | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
|  | 	api "k8s.io/apiserver/pkg/apis/apiserver" | ||||||
|  | 	"k8s.io/client-go/util/cert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	atLeastOneRequiredErrFmt = "at least one %s is required" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	root = field.NewPath("jwt") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration. | ||||||
|  | func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	// This stricter validation is solely based on what the current implementation supports. | ||||||
|  | 	// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up, | ||||||
|  | 	// relax this check to allow 0 authenticators. This will allow us to support the case where | ||||||
|  | 	// API server is initially configured with no authenticators and then authenticators are added | ||||||
|  | 	// later via dynamic config. | ||||||
|  | 	if len(c.JWT) == 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root))) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// This stricter validation is because the --oidc-* flag option is singular. | ||||||
|  | 	// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up, | ||||||
|  | 	// remove the 1 authenticator limit check and add set the limit to 64. | ||||||
|  | 	if len(c.JWT) > 1 { | ||||||
|  | 		allErrs = append(allErrs, field.TooMany(root, len(c.JWT), 1)) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO(aramase): right now we only support a single JWT authenticator as | ||||||
|  | 	// this is wired to the --oidc-* flags. When StructuredAuthenticationConfiguration | ||||||
|  | 	// feature gate is added and wired up, we will remove the 1 authenticator limit | ||||||
|  | 	// check and add validation for duplicate issuers. | ||||||
|  | 	for i, a := range c.JWT { | ||||||
|  | 		fldPath := root.Index(i) | ||||||
|  | 		allErrs = append(allErrs, validateJWTAuthenticator(a, fldPath)...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ValidateJWTAuthenticator validates a given JWTAuthenticator. | ||||||
|  | // This is exported for use in oidc package. | ||||||
|  | func ValidateJWTAuthenticator(authenticator api.JWTAuthenticator) field.ErrorList { | ||||||
|  | 	return validateJWTAuthenticator(authenticator, nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...) | ||||||
|  | 	allErrs = append(allErrs, validateClaimValidationRules(authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"))...) | ||||||
|  | 	allErrs = append(allErrs, validateClaimMappings(authenticator.ClaimMappings, fldPath.Child("claimMappings"))...) | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	allErrs = append(allErrs, validateURL(issuer.URL, fldPath.Child("url"))...) | ||||||
|  | 	allErrs = append(allErrs, validateAudiences(issuer.Audiences, fldPath.Child("audiences"))...) | ||||||
|  | 	allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...) | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	if len(issuerURL) == 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath, "URL is required")) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u, err := url.Parse(issuerURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error())) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  | 	if u.Scheme != "https" { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL scheme must be https")) | ||||||
|  | 	} | ||||||
|  | 	if u.User != nil { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a username or password")) | ||||||
|  | 	} | ||||||
|  | 	if len(u.RawQuery) > 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a query")) | ||||||
|  | 	} | ||||||
|  | 	if len(u.Fragment) > 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a fragment")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateAudiences(audiences []string, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	if len(audiences) == 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath))) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  | 	// This stricter validation is because the --oidc-client-id flag option is singular. | ||||||
|  | 	// This will be removed when we support multiple audiences with the StructuredAuthenticationConfiguration feature gate. | ||||||
|  | 	if len(audiences) > 1 { | ||||||
|  | 		allErrs = append(allErrs, field.TooMany(fldPath, len(audiences), 1)) | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i, audience := range audiences { | ||||||
|  | 		fldPath := fldPath.Index(i) | ||||||
|  | 		if len(audience) == 0 { | ||||||
|  | 			allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty")) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateCertificateAuthority(certificateAuthority string, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	if len(certificateAuthority) == 0 { | ||||||
|  | 		return allErrs | ||||||
|  | 	} | ||||||
|  | 	_, err := cert.NewPoolFromBytes([]byte(certificateAuthority)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		allErrs = append(allErrs, field.Invalid(fldPath, "<omitted>", err.Error())) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateClaimValidationRules(rules []api.ClaimValidationRule, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	seenClaims := sets.NewString() | ||||||
|  | 	for i, rule := range rules { | ||||||
|  | 		fldPath := fldPath.Index(i) | ||||||
|  |  | ||||||
|  | 		if len(rule.Claim) == 0 { | ||||||
|  | 			allErrs = append(allErrs, field.Required(fldPath.Child("claim"), "claim name is required")) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if seenClaims.Has(rule.Claim) { | ||||||
|  | 			allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		seenClaims.Insert(rule.Claim) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateClaimMappings(m api.ClaimMappings, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
|  | 	if len(m.Username.Claim) == 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath.Child("username", "claim"), "claim name is required")) | ||||||
|  | 	} | ||||||
|  | 	// TODO(aramase): when Expression is added to PrefixedClaimOrExpression, check prefix and expression are not both set. | ||||||
|  | 	if m.Username.Prefix == nil { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath.Child("username", "prefix"), "prefix is required")) | ||||||
|  | 	} | ||||||
|  | 	if len(m.Groups.Claim) > 0 && m.Groups.Prefix == nil { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath.Child("groups", "prefix"), "prefix is required when claim is set")) | ||||||
|  | 	} | ||||||
|  | 	if m.Groups.Prefix != nil && len(m.Groups.Claim) == 0 { | ||||||
|  | 		allErrs = append(allErrs, field.Required(fldPath.Child("groups", "claim"), "non-empty claim name is required when prefix is set")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrs | ||||||
|  | } | ||||||
| @@ -0,0 +1,414 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2023 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package validation | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/errors" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
|  | 	api "k8s.io/apiserver/pkg/apis/apiserver" | ||||||
|  | 	certutil "k8s.io/client-go/util/cert" | ||||||
|  | 	"k8s.io/utils/pointer" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestValidateAuthenticationConfiguration(t *testing.T) { | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   *api.AuthenticationConfiguration | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "jwt authenticator is empty", | ||||||
|  | 			in:   &api.AuthenticationConfiguration{}, | ||||||
|  | 			want: "jwt: Required value: at least one jwt is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: ">1 jwt authenticator", | ||||||
|  | 			in: &api.AuthenticationConfiguration{ | ||||||
|  | 				JWT: []api.JWTAuthenticator{ | ||||||
|  | 					{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}}, | ||||||
|  | 					{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: "jwt: Too many: 2: must have at most 1 items", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed issuer validation", | ||||||
|  | 			in: &api.AuthenticationConfiguration{ | ||||||
|  | 				JWT: []api.JWTAuthenticator{ | ||||||
|  | 					{ | ||||||
|  | 						Issuer: api.Issuer{ | ||||||
|  | 							URL:       "invalid-url", | ||||||
|  | 							Audiences: []string{"audience"}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimMappings: api.ClaimMappings{ | ||||||
|  | 							Username: api.PrefixedClaimOrExpression{ | ||||||
|  | 								Claim:  "claim", | ||||||
|  | 								Prefix: pointer.String("prefix"), | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed claimValidationRule validation", | ||||||
|  | 			in: &api.AuthenticationConfiguration{ | ||||||
|  | 				JWT: []api.JWTAuthenticator{ | ||||||
|  | 					{ | ||||||
|  | 						Issuer: api.Issuer{ | ||||||
|  | 							URL:       "https://issuer-url", | ||||||
|  | 							Audiences: []string{"audience"}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||||
|  | 							{ | ||||||
|  | 								Claim:         "foo", | ||||||
|  | 								RequiredValue: "bar", | ||||||
|  | 							}, | ||||||
|  | 							{ | ||||||
|  | 								Claim:         "foo", | ||||||
|  | 								RequiredValue: "baz", | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimMappings: api.ClaimMappings{ | ||||||
|  | 							Username: api.PrefixedClaimOrExpression{ | ||||||
|  | 								Claim:  "claim", | ||||||
|  | 								Prefix: pointer.String("prefix"), | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed claimMapping validation", | ||||||
|  | 			in: &api.AuthenticationConfiguration{ | ||||||
|  | 				JWT: []api.JWTAuthenticator{ | ||||||
|  | 					{ | ||||||
|  | 						Issuer: api.Issuer{ | ||||||
|  | 							URL:       "https://issuer-url", | ||||||
|  | 							Audiences: []string{"audience"}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||||
|  | 							{ | ||||||
|  | 								Claim:         "foo", | ||||||
|  | 								RequiredValue: "bar", | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimMappings: api.ClaimMappings{ | ||||||
|  | 							Username: api.PrefixedClaimOrExpression{ | ||||||
|  | 								Prefix: pointer.String("prefix"), | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: "jwt[0].claimMappings.username.claim: Required value: claim name is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid authentication configuration", | ||||||
|  | 			in: &api.AuthenticationConfiguration{ | ||||||
|  | 				JWT: []api.JWTAuthenticator{ | ||||||
|  | 					{ | ||||||
|  | 						Issuer: api.Issuer{ | ||||||
|  | 							URL:       "https://issuer-url", | ||||||
|  | 							Audiences: []string{"audience"}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||||
|  | 							{ | ||||||
|  | 								Claim:         "foo", | ||||||
|  | 								RequiredValue: "bar", | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 						ClaimMappings: api.ClaimMappings{ | ||||||
|  | 							Username: api.PrefixedClaimOrExpression{ | ||||||
|  | 								Claim:  "sub", | ||||||
|  | 								Prefix: pointer.String("prefix"), | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := ValidateAuthenticationConfiguration(tt.in).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateURL(t *testing.T) { | ||||||
|  | 	fldPath := field.NewPath("issuer", "url") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   string | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "url is empty", | ||||||
|  | 			in:   "", | ||||||
|  | 			want: "issuer.url: Required value: URL is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "url parse error", | ||||||
|  | 			in:   "https://issuer-url:invalid-port", | ||||||
|  | 			want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "url is not https", | ||||||
|  | 			in:   "http://issuer-url", | ||||||
|  | 			want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "url user info is not allowed", | ||||||
|  | 			in:   "https://user:pass@issuer-url", | ||||||
|  | 			want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "url raw query is not allowed", | ||||||
|  | 			in:   "https://issuer-url?query", | ||||||
|  | 			want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "url fragment is not allowed", | ||||||
|  | 			in:   "https://issuer-url#fragment", | ||||||
|  | 			want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid url", | ||||||
|  | 			in:   "https://issuer-url", | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := validateURL(tt.in, fldPath).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("URL validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateAudiences(t *testing.T) { | ||||||
|  | 	fldPath := field.NewPath("issuer", "audiences") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   []string | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "audiences is empty", | ||||||
|  | 			in:   []string{}, | ||||||
|  | 			want: "issuer.audiences: Required value: at least one issuer.audiences is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "at most one audiences is allowed", | ||||||
|  | 			in:   []string{"audience1", "audience2"}, | ||||||
|  | 			want: "issuer.audiences: Too many: 2: must have at most 1 items", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "audience is empty", | ||||||
|  | 			in:   []string{""}, | ||||||
|  | 			want: "issuer.audiences[0]: Required value: audience can't be empty", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid audience", | ||||||
|  | 			in:   []string{"audience"}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := validateAudiences(tt.in, fldPath).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateCertificateAuthority(t *testing.T) { | ||||||
|  | 	fldPath := field.NewPath("issuer", "certificateAuthority") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   func() string | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "invalid certificate authority", | ||||||
|  | 			in:   func() string { return "invalid" }, | ||||||
|  | 			want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "certificate authority is empty", | ||||||
|  | 			in:   func() string { return "" }, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid certificate authority", | ||||||
|  | 			in: func() string { | ||||||
|  | 				caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||||||
|  | 				if err != nil { | ||||||
|  | 					t.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 				caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) | ||||||
|  | 				if err != nil { | ||||||
|  | 					t.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 				return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})) | ||||||
|  | 			}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestClaimValidationRules(t *testing.T) { | ||||||
|  | 	fldPath := field.NewPath("issuer", "claimValidationRules") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   []api.ClaimValidationRule | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "claim validation rule claim is empty", | ||||||
|  | 			in:   []api.ClaimValidationRule{{Claim: ""}}, | ||||||
|  | 			want: "issuer.claimValidationRules[0].claim: Required value: claim name is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "duplicate claim", | ||||||
|  | 			in: []api.ClaimValidationRule{{ | ||||||
|  | 				Claim: "claim", RequiredValue: "value1"}, | ||||||
|  | 				{Claim: "claim", RequiredValue: "value2"}, | ||||||
|  | 			}, | ||||||
|  | 			want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid claim validation rule", | ||||||
|  | 			in:   []api.ClaimValidationRule{{Claim: "claim", RequiredValue: "value"}}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid claim validation rule with multiple rules", | ||||||
|  | 			in: []api.ClaimValidationRule{ | ||||||
|  | 				{Claim: "claim1", RequiredValue: "value1"}, | ||||||
|  | 				{Claim: "claim2", RequiredValue: "value2"}, | ||||||
|  | 			}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := validateClaimValidationRules(tt.in, fldPath).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateClaimMappings(t *testing.T) { | ||||||
|  | 	fldPath := field.NewPath("issuer", "claimMappings") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  | 		in   api.ClaimMappings | ||||||
|  | 		want string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "username claim is empty", | ||||||
|  | 			in:   api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "", Prefix: pointer.String("prefix")}}, | ||||||
|  | 			want: "issuer.claimMappings.username.claim: Required value: claim name is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "username prefix is empty", | ||||||
|  | 			in:   api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "claim"}}, | ||||||
|  | 			want: "issuer.claimMappings.username.prefix: Required value: prefix is required", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "groups prefix is empty", | ||||||
|  | 			in: api.ClaimMappings{ | ||||||
|  | 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||||
|  | 				Groups:   api.PrefixedClaimOrExpression{Claim: "claim"}, | ||||||
|  | 			}, | ||||||
|  | 			want: "issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "groups prefix set but claim is empty", | ||||||
|  | 			in: api.ClaimMappings{ | ||||||
|  | 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||||
|  | 				Groups:   api.PrefixedClaimOrExpression{Prefix: pointer.String("prefix")}, | ||||||
|  | 			}, | ||||||
|  | 			want: "issuer.claimMappings.groups.claim: Required value: non-empty claim name is required when prefix is set", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "valid claim mappings", | ||||||
|  | 			in: api.ClaimMappings{ | ||||||
|  | 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||||
|  | 				Groups:   api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||||
|  | 			}, | ||||||
|  | 			want: "", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range testCases { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := validateClaimMappings(tt.in, fldPath).ToAggregate() | ||||||
|  | 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||||
|  | 				t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func errString(errs errors.Aggregate) string { | ||||||
|  | 	if errs != nil { | ||||||
|  | 		return errs.Error() | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
| @@ -32,11 +32,9 @@ import ( | |||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"sync/atomic" | 	"sync/atomic" | ||||||
| @@ -46,6 +44,8 @@ import ( | |||||||
|  |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/net" | 	"k8s.io/apimachinery/pkg/util/net" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||||
|  | 	apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||||
| 	"k8s.io/apiserver/pkg/authentication/user" | 	"k8s.io/apiserver/pkg/authentication/user" | ||||||
| 	certutil "k8s.io/client-go/util/cert" | 	certutil "k8s.io/client-go/util/cert" | ||||||
| @@ -59,50 +59,17 @@ var ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss" | 	// JWTAuthenticator is the authenticator that will be used to verify the JWT. | ||||||
| 	// field of all tokens produced by the provider and is used for configuration | 	JWTAuthenticator apiserver.JWTAuthenticator | ||||||
| 	// discovery. |  | ||||||
| 	// |  | ||||||
| 	// The URL is usually the provider's URL without a path, for example |  | ||||||
| 	// "https://accounts.google.com" or "https://login.salesforce.com". |  | ||||||
| 	// |  | ||||||
| 	// The provider must implement configuration discovery. |  | ||||||
| 	// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig |  | ||||||
| 	IssuerURL string |  | ||||||
|  |  | ||||||
| 	// Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer. | 	// Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer. | ||||||
| 	KeySet oidc.KeySet | 	KeySet oidc.KeySet | ||||||
|  |  | ||||||
| 	// ClientID the JWT must be issued for, the "sub" field. This plugin only trusts a single |  | ||||||
| 	// client to ensure the plugin can be used with public providers. |  | ||||||
| 	// |  | ||||||
| 	// The plugin supports the "authorized party" OpenID Connect claim, which allows |  | ||||||
| 	// specialized providers to issue tokens to a client for a different client. |  | ||||||
| 	// See: https://openid.net/specs/openid-connect-core-1_0.html#IDToken |  | ||||||
| 	ClientID string |  | ||||||
|  |  | ||||||
| 	// PEM encoded root certificate contents of the provider.  Mutually exclusive with Client. | 	// PEM encoded root certificate contents of the provider.  Mutually exclusive with Client. | ||||||
| 	CAContentProvider CAContentProvider | 	CAContentProvider CAContentProvider | ||||||
|  |  | ||||||
| 	// Optional http.Client used to make all requests to the remote issuer.  Mutually exclusive with CAContentProvider. | 	// Optional http.Client used to make all requests to the remote issuer.  Mutually exclusive with CAContentProvider. | ||||||
| 	Client *http.Client | 	Client *http.Client | ||||||
|  |  | ||||||
| 	// UsernameClaim is the JWT field to use as the user's username. |  | ||||||
| 	UsernameClaim string |  | ||||||
|  |  | ||||||
| 	// UsernamePrefix, if specified, causes claims mapping to username to be prefix with |  | ||||||
| 	// the provided value. A value "oidc:" would result in usernames like "oidc:john". |  | ||||||
| 	UsernamePrefix string |  | ||||||
|  |  | ||||||
| 	// GroupsClaim, if specified, causes the OIDCAuthenticator to try to populate the user's |  | ||||||
| 	// groups with an ID Token field. If the GroupsClaim field is present in an ID Token the value |  | ||||||
| 	// must be a string or list of strings. |  | ||||||
| 	GroupsClaim string |  | ||||||
|  |  | ||||||
| 	// GroupsPrefix, if specified, causes claims mapping to group names to be prefixed with the |  | ||||||
| 	// value. A value "oidc:" would result in groups like "oidc:engineering" and "oidc:marketing". |  | ||||||
| 	GroupsPrefix string |  | ||||||
|  |  | ||||||
| 	// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that | 	// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that | ||||||
| 	// can be used by the provider to sign tokens. | 	// can be used by the provider to sign tokens. | ||||||
| 	// | 	// | ||||||
| @@ -114,10 +81,6 @@ type Options struct { | |||||||
| 	// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation | 	// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation | ||||||
| 	SupportedSigningAlgs []string | 	SupportedSigningAlgs []string | ||||||
|  |  | ||||||
| 	// RequiredClaims, if specified, causes the OIDCAuthenticator to verify that all the |  | ||||||
| 	// required claims key value pairs are present in the ID Token. |  | ||||||
| 	RequiredClaims map[string]string |  | ||||||
|  |  | ||||||
| 	// now is used for testing. It defaults to time.Now. | 	// now is used for testing. It defaults to time.Now. | ||||||
| 	now func() time.Time | 	now func() time.Time | ||||||
| } | } | ||||||
| @@ -192,13 +155,7 @@ func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier { | |||||||
| } | } | ||||||
|  |  | ||||||
| type Authenticator struct { | type Authenticator struct { | ||||||
| 	issuerURL string | 	jwtAuthenticator apiserver.JWTAuthenticator | ||||||
|  |  | ||||||
| 	usernameClaim  string |  | ||||||
| 	usernamePrefix string |  | ||||||
| 	groupsClaim    string |  | ||||||
| 	groupsPrefix   string |  | ||||||
| 	requiredClaims map[string]string |  | ||||||
|  |  | ||||||
| 	// Contains an *oidc.IDTokenVerifier. Do not access directly use the | 	// Contains an *oidc.IDTokenVerifier. Do not access directly use the | ||||||
| 	// idTokenVerifier method. | 	// idTokenVerifier method. | ||||||
| @@ -240,19 +197,10 @@ var allowedSigningAlgs = map[string]bool{ | |||||||
| } | } | ||||||
|  |  | ||||||
| func New(opts Options) (*Authenticator, error) { | func New(opts Options) (*Authenticator, error) { | ||||||
| 	url, err := url.Parse(opts.IssuerURL) | 	if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil { | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if url.Scheme != "https" { |  | ||||||
| 		return nil, fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", opts.IssuerURL, url.Scheme) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if opts.UsernameClaim == "" { |  | ||||||
| 		return nil, errors.New("no username claim provided") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	supportedSigningAlgs := opts.SupportedSigningAlgs | 	supportedSigningAlgs := opts.SupportedSigningAlgs | ||||||
| 	if len(supportedSigningAlgs) == 0 { | 	if len(supportedSigningAlgs) == 0 { | ||||||
| 		// RS256 is the default recommended by OpenID Connect and an 'alg' value | 		// RS256 is the default recommended by OpenID Connect and an 'alg' value | ||||||
| @@ -273,6 +221,7 @@ func New(opts Options) (*Authenticator, error) { | |||||||
|  |  | ||||||
| 	if client == nil { | 	if client == nil { | ||||||
| 		var roots *x509.CertPool | 		var roots *x509.CertPool | ||||||
|  | 		var err error | ||||||
| 		if opts.CAContentProvider != nil { | 		if opts.CAContentProvider != nil { | ||||||
| 			// TODO(enj): make this reload CA data dynamically | 			// TODO(enj): make this reload CA data dynamically | ||||||
| 			roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent()) | 			roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent()) | ||||||
| @@ -302,35 +251,30 @@ func New(opts Options) (*Authenticator, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	verifierConfig := &oidc.Config{ | 	verifierConfig := &oidc.Config{ | ||||||
| 		ClientID:             opts.ClientID, | 		ClientID:             opts.JWTAuthenticator.Issuer.Audiences[0], | ||||||
| 		SupportedSigningAlgs: supportedSigningAlgs, | 		SupportedSigningAlgs: supportedSigningAlgs, | ||||||
| 		Now:                  now, | 		Now:                  now, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var resolver *claimResolver | 	var resolver *claimResolver | ||||||
| 	if opts.GroupsClaim != "" { | 	if opts.JWTAuthenticator.ClaimMappings.Groups.Claim != "" { | ||||||
| 		resolver = newClaimResolver(opts.GroupsClaim, client, verifierConfig) | 		resolver = newClaimResolver(opts.JWTAuthenticator.ClaimMappings.Groups.Claim, client, verifierConfig) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	authenticator := &Authenticator{ | 	authenticator := &Authenticator{ | ||||||
| 		issuerURL:      opts.IssuerURL, | 		jwtAuthenticator: opts.JWTAuthenticator, | ||||||
| 		usernameClaim:  opts.UsernameClaim, | 		cancel:           cancel, | ||||||
| 		usernamePrefix: opts.UsernamePrefix, | 		resolver:         resolver, | ||||||
| 		groupsClaim:    opts.GroupsClaim, |  | ||||||
| 		groupsPrefix:   opts.GroupsPrefix, |  | ||||||
| 		requiredClaims: opts.RequiredClaims, |  | ||||||
| 		cancel:         cancel, |  | ||||||
| 		resolver:       resolver, |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if opts.KeySet != nil { | 	if opts.KeySet != nil { | ||||||
| 		// We already have a key set, synchronously initialize the verifier. | 		// We already have a key set, synchronously initialize the verifier. | ||||||
| 		authenticator.setVerifier(oidc.NewVerifier(opts.IssuerURL, opts.KeySet, verifierConfig)) | 		authenticator.setVerifier(oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig)) | ||||||
| 	} else { | 	} else { | ||||||
| 		// Asynchronously attempt to initialize the authenticator. This enables | 		// Asynchronously attempt to initialize the authenticator. This enables | ||||||
| 		// self-hosted providers, providers that run on top of Kubernetes itself. | 		// self-hosted providers, providers that run on top of Kubernetes itself. | ||||||
| 		go wait.PollImmediateUntil(10*time.Second, func() (done bool, err error) { | 		go wait.PollImmediateUntil(10*time.Second, func() (done bool, err error) { | ||||||
| 			provider, err := oidc.NewProvider(ctx, opts.IssuerURL) | 			provider, err := oidc.NewProvider(ctx, opts.JWTAuthenticator.Issuer.URL) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				klog.Errorf("oidc authenticator: initializing plugin: %v", err) | 				klog.Errorf("oidc authenticator: initializing plugin: %v", err) | ||||||
| 				return false, nil | 				return false, nil | ||||||
| @@ -552,7 +496,7 @@ func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaim | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { | func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { | ||||||
| 	if !hasCorrectIssuer(a.issuerURL, token) { | 	if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) { | ||||||
| 		return nil, false, nil | 		return nil, false, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -577,11 +521,11 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var username string | 	var username string | ||||||
| 	if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil { | 	if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Username.Claim, &username); err != nil { | ||||||
| 		return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err) | 		return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.jwtAuthenticator.ClaimMappings.Username.Claim, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.usernameClaim == "email" { | 	if a.jwtAuthenticator.ClaimMappings.Username.Claim == "email" { | ||||||
| 		// If the email_verified claim is present, ensure the email is valid. | 		// If the email_verified claim is present, ensure the email is valid. | ||||||
| 		// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims | 		// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims | ||||||
| 		if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified { | 		if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified { | ||||||
| @@ -597,33 +541,36 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.usernamePrefix != "" { | 	if a.jwtAuthenticator.ClaimMappings.Username.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Username.Prefix != "" { | ||||||
| 		username = a.usernamePrefix + username | 		username = *a.jwtAuthenticator.ClaimMappings.Username.Prefix + username | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	info := &user.DefaultInfo{Name: username} | 	info := &user.DefaultInfo{Name: username} | ||||||
| 	if a.groupsClaim != "" { | 	if a.jwtAuthenticator.ClaimMappings.Groups.Claim != "" { | ||||||
| 		if _, ok := c[a.groupsClaim]; ok { | 		if _, ok := c[a.jwtAuthenticator.ClaimMappings.Groups.Claim]; ok { | ||||||
| 			// Some admins want to use string claims like "role" as the group value. | 			// Some admins want to use string claims like "role" as the group value. | ||||||
| 			// Allow the group claim to be a single string instead of an array. | 			// Allow the group claim to be a single string instead of an array. | ||||||
| 			// | 			// | ||||||
| 			// See: https://github.com/kubernetes/kubernetes/issues/33290 | 			// See: https://github.com/kubernetes/kubernetes/issues/33290 | ||||||
| 			var groups stringOrArray | 			var groups stringOrArray | ||||||
| 			if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil { | 			if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Groups.Claim, &groups); err != nil { | ||||||
| 				return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err) | 				return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.jwtAuthenticator.ClaimMappings.Groups.Claim, err) | ||||||
| 			} | 			} | ||||||
| 			info.Groups = []string(groups) | 			info.Groups = []string(groups) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.groupsPrefix != "" { | 	if a.jwtAuthenticator.ClaimMappings.Groups.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Groups.Prefix != "" { | ||||||
| 		for i, group := range info.Groups { | 		for i, group := range info.Groups { | ||||||
| 			info.Groups[i] = a.groupsPrefix + group | 			info.Groups[i] = *a.jwtAuthenticator.ClaimMappings.Groups.Prefix + group | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// check to ensure all required claims are present in the ID token and have matching values. | 	// check to ensure all required claims are present in the ID token and have matching values. | ||||||
| 	for claim, value := range a.requiredClaims { | 	for _, claimValidationRule := range a.jwtAuthenticator.ClaimValidationRules { | ||||||
|  | 		claim := claimValidationRule.Claim | ||||||
|  | 		value := claimValidationRule.RequiredValue | ||||||
|  |  | ||||||
| 		if !c.hasClaim(claim) { | 		if !c.hasClaim(claim) { | ||||||
| 			return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim) | 			return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim) | ||||||
| 		} | 		} | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -1455,6 +1455,7 @@ k8s.io/apiserver/pkg/apis/apiserver/install | |||||||
| k8s.io/apiserver/pkg/apis/apiserver/v1 | k8s.io/apiserver/pkg/apis/apiserver/v1 | ||||||
| k8s.io/apiserver/pkg/apis/apiserver/v1alpha1 | k8s.io/apiserver/pkg/apis/apiserver/v1alpha1 | ||||||
| k8s.io/apiserver/pkg/apis/apiserver/v1beta1 | k8s.io/apiserver/pkg/apis/apiserver/v1beta1 | ||||||
|  | k8s.io/apiserver/pkg/apis/apiserver/validation | ||||||
| k8s.io/apiserver/pkg/apis/audit | k8s.io/apiserver/pkg/apis/audit | ||||||
| k8s.io/apiserver/pkg/apis/audit/install | k8s.io/apiserver/pkg/apis/audit/install | ||||||
| k8s.io/apiserver/pkg/apis/audit/v1 | k8s.io/apiserver/pkg/apis/audit/v1 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Anish Ramasekar
					Anish Ramasekar