Merge pull request #118984 from aramase/aramase/c/kep_3331_wiring_flag_with_api

[StructuredAuthenticationConfig] Create struct for authn config and re-wire OIDC flags to use it
This commit is contained in:
Kubernetes Prow Robot 2023-08-25 11:52:55 -07:00 committed by GitHub
commit 0e86fa5115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2249 additions and 373 deletions

View File

@ -19,11 +19,11 @@ package authenticator
import (
"errors"
"fmt"
"os"
"time"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/group"
@ -55,15 +55,8 @@ type Config struct {
BootstrapToken bool
TokenAuthFile string
OIDCIssuerURL string
OIDCClientID string
OIDCCAFile string
OIDCUsernameClaim string
OIDCUsernamePrefix string
OIDCGroupsClaim string
OIDCGroupsPrefix string
AuthenticationConfig *apiserver.AuthenticationConfiguration
OIDCSigningAlgs []string
OIDCRequiredClaims map[string]string
ServiceAccountKeyFiles []string
ServiceAccountLookup bool
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
// simply returns an error, the OpenID Connect plugin may query the provider to
// update the keys, causing performance hits.
if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 {
// TODO(enj): wire up the Notifier and ControllerRunner bits when OIDC supports CA reload
var oidcCAContent oidc.CAContentProvider
if len(config.OIDCCAFile) != 0 {
var oidcCAErr error
oidcCAContent, oidcCAErr = staticCAContentProviderFromFile("oidc-authenticator", config.OIDCCAFile)
if oidcCAErr != nil {
return nil, nil, oidcCAErr
if config.AuthenticationConfig != nil {
for _, jwtAuthenticator := range config.AuthenticationConfig.JWT {
var oidcCAContent oidc.CAContentProvider
if len(jwtAuthenticator.Issuer.CertificateAuthority) > 0 {
var oidcCAError error
oidcCAContent, oidcCAError = dynamiccertificates.NewStaticCAContent("oidc-authenticator", []byte(jwtAuthenticator.Issuer.CertificateAuthority))
if oidcCAError != nil {
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 {
webhookTokenAuth, err := newWebhookTokenAuthenticator(config)
if err != nil {
@ -243,31 +231,6 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e
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
func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) {
allPublicKeys := []interface{}{}
@ -318,12 +281,3 @@ func newWebhookTokenAuthenticator(config Config) (authenticator.Token, error) {
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)
}

View File

@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"
@ -28,6 +29,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"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"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/egressselector"
@ -41,6 +44,7 @@ import (
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap"
"k8s.io/utils/pointer"
)
// BuiltInAuthenticationOptions contains all build-in authentication options for API Server
@ -397,16 +401,68 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
}
}
if o.OIDC != nil {
ret.OIDCCAFile = o.OIDC.CAFile
ret.OIDCClientID = o.OIDC.ClientID
ret.OIDCGroupsClaim = o.OIDC.GroupsClaim
ret.OIDCGroupsPrefix = o.OIDC.GroupsPrefix
ret.OIDCIssuerURL = o.OIDC.IssuerURL
ret.OIDCUsernameClaim = o.OIDC.UsernameClaim
ret.OIDCUsernamePrefix = o.OIDC.UsernamePrefix
if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 {
usernamePrefix := o.OIDC.UsernamePrefix
if o.OIDC.UsernamePrefix == "" && o.OIDC.UsernameClaim != "email" {
// Legacy CLI flag behavior. If a usernamePrefix isn't provided, prefix all claims other than "email"
// with the issuerURL.
//
// 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.OIDCRequiredClaims = o.OIDC.RequiredClaims
}
if o.RequestHeader != nil {

View File

@ -17,6 +17,7 @@ limitations under the License.
package options
import (
"os"
"reflect"
"strings"
"testing"
@ -27,11 +28,13 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
apiserveroptions "k8s.io/apiserver/pkg/server/options"
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
"k8s.io/utils/pointer"
)
func TestAuthenticationValidate(t *testing.T) {
@ -50,7 +53,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -63,7 +66,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
},
testSA: &ServiceAccountAuthenticationOptions{
Issuers: []string{"http://foo.bar.com"},
@ -76,7 +79,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -89,7 +92,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -102,7 +105,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -115,7 +118,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -128,7 +131,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -141,7 +144,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -156,7 +159,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -171,7 +174,7 @@ func TestAuthenticationValidate(t *testing.T) {
testOIDC: &OIDCAuthenticationOptions{
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
testSA: &ServiceAccountAuthenticationOptions{
@ -228,10 +231,10 @@ func TestToAuthenticationConfig(t *testing.T) {
Enable: false,
},
OIDC: &OIDCAuthenticationOptions{
CAFile: "/testCAFile",
CAFile: "testdata/root.pem",
UsernameClaim: "sub",
SigningAlgs: []string{"RS256"},
IssuerURL: "testIssuerURL",
IssuerURL: "https://testIssuerURL",
ClientID: "testClientID",
},
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{
@ -253,15 +256,27 @@ func TestToAuthenticationConfig(t *testing.T) {
}
expectConfig := kubeauthenticator.Config{
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
Anonymous: false,
BootstrapToken: false,
ClientCAContentProvider: nil, // this is nil because you can't compare functions
TokenAuthFile: "/testTokenFile",
OIDCIssuerURL: "testIssuerURL",
OIDCClientID: "testClientID",
OIDCCAFile: "/testCAFile",
OIDCUsernameClaim: "sub",
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
Anonymous: false,
BootstrapToken: false,
ClientCAContentProvider: nil, // this is nil because you can't compare functions
TokenAuthFile: "/testTokenFile",
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#"),
},
},
},
},
},
OIDCSigningAlgs: []string{"RS256"},
ServiceAccountLookup: true,
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()
if err != nil {
t.Fatal(err)
@ -385,3 +406,221 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
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))
}
})
}
}

View File

@ -157,3 +157,44 @@ type TracingConfiguration struct {
// Embed the component config tracing configuration struct
tracingapi.TracingConfiguration
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// AuthenticationConfiguration provides versioned configuration for authentication.
type AuthenticationConfiguration struct {
metav1.TypeMeta
JWT []JWTAuthenticator
}
// JWTAuthenticator provides the configuration for a single JWT authenticator.
type JWTAuthenticator struct {
Issuer Issuer
ClaimValidationRules []ClaimValidationRule
ClaimMappings ClaimMappings
}
// Issuer provides the configuration for a external provider specific settings.
type Issuer struct {
URL string
CertificateAuthority string
Audiences []string
}
// ClaimValidationRule provides the configuration for a single claim validation rule.
type ClaimValidationRule struct {
Claim string
RequiredValue string
}
// ClaimMappings provides the configuration for claim mapping
type ClaimMappings struct {
Username PrefixedClaimOrExpression
Groups PrefixedClaimOrExpression
}
// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.
type PrefixedClaimOrExpression struct {
Claim string
Prefix *string
}

View File

@ -158,3 +158,113 @@ type TracingConfiguration struct {
// Embed the component config tracing configuration struct
tracingapi.TracingConfiguration `json:",inline"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// AuthenticationConfiguration provides versioned configuration for authentication.
type AuthenticationConfiguration struct {
metav1.TypeMeta
// jwt is a list of authenticator to authenticate Kubernetes users using
// JWT compliant tokens. The authenticator will attempt to parse a raw ID token,
// verify it's been signed by the configured issuer. The public key to verify the
// signature is discovered from the issuer's public endpoint using OIDC discovery.
// For an incoming token, each JWT authenticator will be attempted in
// the order in which it is specified in this list. Note however that
// other authenticators may run before or after the JWT authenticators.
// The specific position of JWT authenticators in relation to other
// authenticators is neither defined nor stable across releases. Since
// each JWT authenticator must have a unique issuer URL, at most one
// JWT authenticator will attempt to cryptographically validate the token.
JWT []JWTAuthenticator `json:"jwt"`
}
// JWTAuthenticator provides the configuration for a single JWT authenticator.
type JWTAuthenticator struct {
// issuer contains the basic OIDC provider connection options.
// +required
Issuer Issuer `json:"issuer"`
// claimValidationRules are rules that are applied to validate token claims to authenticate users.
// +optional
ClaimValidationRules []ClaimValidationRule `json:"claimValidationRules,omitempty"`
// claimMappings points claims of a token to be treated as user attributes.
// +required
ClaimMappings ClaimMappings `json:"claimMappings"`
}
// Issuer provides the configuration for a external provider specific settings.
type Issuer struct {
// url points to the issuer URL in a format https://url or https://url/path.
// This must match the "iss" claim in the presented JWT, and the issuer returned from discovery.
// Same value as the --oidc-issuer-url flag.
// Used to fetch discovery information unless overridden by discoveryURL.
// Required to be unique.
// Note that egress selection configuration is not used for this network connection.
// +required
URL string `json:"url"`
// certificateAuthority contains PEM-encoded certificate authority certificates
// used to validate the connection when fetching discovery information.
// If unset, the system verifier is used.
// Same value as the content of the file referenced by the --oidc-ca-file flag.
// +optional
CertificateAuthority string `json:"certificateAuthority,omitempty"`
// audiences is the set of acceptable audiences the JWT must be issued to.
// At least one of the entries must match the "aud" claim in presented JWTs.
// Same value as the --oidc-client-id flag (though this field supports an array).
// Required to be non-empty.
// +required
Audiences []string `json:"audiences"`
}
// ClaimValidationRule provides the configuration for a single claim validation rule.
type ClaimValidationRule struct {
// claim is the name of a required claim.
// Same as --oidc-required-claim flag.
// Only string claim keys are supported.
// +required
Claim string `json:"claim"`
// requiredValue is the value of a required claim.
// Same as --oidc-required-claim flag.
// Only string claim values are supported.
// If claim is set and requiredValue is not set, the claim must be present with a value set to the empty string.
// +optional
RequiredValue string `json:"requiredValue"`
}
// ClaimMappings provides the configuration for claim mapping
type ClaimMappings struct {
// username represents an option for the username attribute.
// The claim's value must be a singular string.
// Same as the --oidc-username-claim and --oidc-username-prefix flags.
//
// In the flag based approach, the --oidc-username-claim and --oidc-username-prefix are optional. If --oidc-username-claim is not set,
// the default value is "sub". For the authentication config, there is no defaulting for claim or prefix. The claim and prefix must be set explicitly.
// For claim, if --oidc-username-claim was not set with legacy flag approach, configure username.claim="sub" in the authentication config.
// For prefix:
// (1) --oidc-username-prefix="-", no prefix was added to the username. For the same behavior using authentication config,
// set username.prefix=""
// (2) --oidc-username-prefix="" and --oidc-username-claim != "email", prefix was "<value of --oidc-issuer-url>#". For the same
// behavior using authentication config, set username.prefix="<value of issuer.url>#"
// (3) --oidc-username-prefix="<value>". For the same behavior using authentication config, set username.prefix="<value>"
// +required
Username PrefixedClaimOrExpression `json:"username"`
// groups represents an option for the groups attribute.
// The claim's value must be a string or string array claim.
// // If groups.claim is set, the prefix must be specified (and can be the empty string).
// +optional
Groups PrefixedClaimOrExpression `json:"groups,omitempty"`
}
// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.
type PrefixedClaimOrExpression struct {
// claim is the JWT claim to use.
// +optional
Claim string `json:"claim"`
// prefix is prepended to claim's value to prevent clashes with existing names.
// +required
Prefix *string `json:"prefix"`
}

View File

@ -56,6 +56,36 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*AuthenticationConfiguration)(nil), (*apiserver.AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(a.(*AuthenticationConfiguration), b.(*apiserver.AuthenticationConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.AuthenticationConfiguration)(nil), (*AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(a.(*apiserver.AuthenticationConfiguration), b.(*AuthenticationConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*ClaimMappings)(nil), (*apiserver.ClaimMappings)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(a.(*ClaimMappings), b.(*apiserver.ClaimMappings), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.ClaimMappings)(nil), (*ClaimMappings)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(a.(*apiserver.ClaimMappings), b.(*ClaimMappings), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*ClaimValidationRule)(nil), (*apiserver.ClaimValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(a.(*ClaimValidationRule), b.(*apiserver.ClaimValidationRule), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.ClaimValidationRule)(nil), (*ClaimValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(a.(*apiserver.ClaimValidationRule), b.(*ClaimValidationRule), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*Connection)(nil), (*apiserver.Connection)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_Connection_To_apiserver_Connection(a.(*Connection), b.(*apiserver.Connection), scope)
}); err != nil {
@ -81,6 +111,36 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*Issuer)(nil), (*apiserver.Issuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_Issuer_To_apiserver_Issuer(a.(*Issuer), b.(*apiserver.Issuer), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.Issuer)(nil), (*Issuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_Issuer_To_v1alpha1_Issuer(a.(*apiserver.Issuer), b.(*Issuer), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*JWTAuthenticator)(nil), (*apiserver.JWTAuthenticator)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(a.(*JWTAuthenticator), b.(*apiserver.JWTAuthenticator), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.JWTAuthenticator)(nil), (*JWTAuthenticator)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(a.(*apiserver.JWTAuthenticator), b.(*JWTAuthenticator), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*PrefixedClaimOrExpression)(nil), (*apiserver.PrefixedClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(a.(*PrefixedClaimOrExpression), b.(*apiserver.PrefixedClaimOrExpression), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.PrefixedClaimOrExpression)(nil), (*PrefixedClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(a.(*apiserver.PrefixedClaimOrExpression), b.(*PrefixedClaimOrExpression), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*TCPTransport)(nil), (*apiserver.TCPTransport)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_TCPTransport_To_apiserver_TCPTransport(a.(*TCPTransport), b.(*apiserver.TCPTransport), scope)
}); err != nil {
@ -183,6 +243,78 @@ func Convert_apiserver_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginC
return autoConvert_apiserver_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(in, out, s)
}
func autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error {
out.JWT = *(*[]apiserver.JWTAuthenticator)(unsafe.Pointer(&in.JWT))
return nil
}
// Convert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration is an autogenerated conversion function.
func Convert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error {
return autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in, out, s)
}
func autoConvert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *apiserver.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error {
out.JWT = *(*[]JWTAuthenticator)(unsafe.Pointer(&in.JWT))
return nil
}
// Convert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration is an autogenerated conversion function.
func Convert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *apiserver.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in, out, s)
}
func autoConvert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(in *ClaimMappings, out *apiserver.ClaimMappings, s conversion.Scope) error {
if err := Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(&in.Username, &out.Username, s); err != nil {
return err
}
if err := Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings is an autogenerated conversion function.
func Convert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(in *ClaimMappings, out *apiserver.ClaimMappings, s conversion.Scope) error {
return autoConvert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(in, out, s)
}
func autoConvert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in *apiserver.ClaimMappings, out *ClaimMappings, s conversion.Scope) error {
if err := Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(&in.Username, &out.Username, s); err != nil {
return err
}
if err := Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil {
return err
}
return nil
}
// Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings is an autogenerated conversion function.
func Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in *apiserver.ClaimMappings, out *ClaimMappings, s conversion.Scope) error {
return autoConvert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in, out, s)
}
func autoConvert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(in *ClaimValidationRule, out *apiserver.ClaimValidationRule, s conversion.Scope) error {
out.Claim = in.Claim
out.RequiredValue = in.RequiredValue
return nil
}
// Convert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule is an autogenerated conversion function.
func Convert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(in *ClaimValidationRule, out *apiserver.ClaimValidationRule, s conversion.Scope) error {
return autoConvert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(in, out, s)
}
func autoConvert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in *apiserver.ClaimValidationRule, out *ClaimValidationRule, s conversion.Scope) error {
out.Claim = in.Claim
out.RequiredValue = in.RequiredValue
return nil
}
// Convert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule is an autogenerated conversion function.
func Convert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in *apiserver.ClaimValidationRule, out *ClaimValidationRule, s conversion.Scope) error {
return autoConvert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in, out, s)
}
func autoConvert_v1alpha1_Connection_To_apiserver_Connection(in *Connection, out *apiserver.Connection, s conversion.Scope) error {
out.ProxyProtocol = apiserver.ProtocolType(in.ProxyProtocol)
out.Transport = (*apiserver.Transport)(unsafe.Pointer(in.Transport))
@ -266,6 +398,84 @@ func Convert_apiserver_EgressSelectorConfiguration_To_v1alpha1_EgressSelectorCon
return autoConvert_apiserver_EgressSelectorConfiguration_To_v1alpha1_EgressSelectorConfiguration(in, out, s)
}
func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issuer, s conversion.Scope) error {
out.URL = in.URL
out.CertificateAuthority = in.CertificateAuthority
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
return nil
}
// Convert_v1alpha1_Issuer_To_apiserver_Issuer is an autogenerated conversion function.
func Convert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issuer, s conversion.Scope) error {
return autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in, out, s)
}
func autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out *Issuer, s conversion.Scope) error {
out.URL = in.URL
out.CertificateAuthority = in.CertificateAuthority
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
return nil
}
// Convert_apiserver_Issuer_To_v1alpha1_Issuer is an autogenerated conversion function.
func Convert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out *Issuer, s conversion.Scope) error {
return autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in, out, s)
}
func autoConvert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(in *JWTAuthenticator, out *apiserver.JWTAuthenticator, s conversion.Scope) error {
if err := Convert_v1alpha1_Issuer_To_apiserver_Issuer(&in.Issuer, &out.Issuer, s); err != nil {
return err
}
out.ClaimValidationRules = *(*[]apiserver.ClaimValidationRule)(unsafe.Pointer(&in.ClaimValidationRules))
if err := Convert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(&in.ClaimMappings, &out.ClaimMappings, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator is an autogenerated conversion function.
func Convert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(in *JWTAuthenticator, out *apiserver.JWTAuthenticator, s conversion.Scope) error {
return autoConvert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(in, out, s)
}
func autoConvert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *apiserver.JWTAuthenticator, out *JWTAuthenticator, s conversion.Scope) error {
if err := Convert_apiserver_Issuer_To_v1alpha1_Issuer(&in.Issuer, &out.Issuer, s); err != nil {
return err
}
out.ClaimValidationRules = *(*[]ClaimValidationRule)(unsafe.Pointer(&in.ClaimValidationRules))
if err := Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(&in.ClaimMappings, &out.ClaimMappings, s); err != nil {
return err
}
return nil
}
// Convert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator is an autogenerated conversion function.
func Convert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *apiserver.JWTAuthenticator, out *JWTAuthenticator, s conversion.Scope) error {
return autoConvert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in, out, s)
}
func autoConvert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(in *PrefixedClaimOrExpression, out *apiserver.PrefixedClaimOrExpression, s conversion.Scope) error {
out.Claim = in.Claim
out.Prefix = (*string)(unsafe.Pointer(in.Prefix))
return nil
}
// Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression is an autogenerated conversion function.
func Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(in *PrefixedClaimOrExpression, out *apiserver.PrefixedClaimOrExpression, s conversion.Scope) error {
return autoConvert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(in, out, s)
}
func autoConvert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in *apiserver.PrefixedClaimOrExpression, out *PrefixedClaimOrExpression, s conversion.Scope) error {
out.Claim = in.Claim
out.Prefix = (*string)(unsafe.Pointer(in.Prefix))
return nil
}
// Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression is an autogenerated conversion function.
func Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in *apiserver.PrefixedClaimOrExpression, out *PrefixedClaimOrExpression, s conversion.Scope) error {
return autoConvert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in, out, s)
}
func autoConvert_v1alpha1_TCPTransport_To_apiserver_TCPTransport(in *TCPTransport, out *apiserver.TCPTransport, s conversion.Scope) error {
out.URL = in.URL
out.TLSConfig = (*apiserver.TLSConfig)(unsafe.Pointer(in.TLSConfig))

View File

@ -78,6 +78,72 @@ func (in *AdmissionPluginConfiguration) DeepCopy() *AdmissionPluginConfiguration
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.JWT != nil {
in, out := &in.JWT, &out.JWT
*out = make([]JWTAuthenticator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfiguration.
func (in *AuthenticationConfiguration) DeepCopy() *AuthenticationConfiguration {
if in == nil {
return nil
}
out := new(AuthenticationConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AuthenticationConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) {
*out = *in
in.Username.DeepCopyInto(&out.Username)
in.Groups.DeepCopyInto(&out.Groups)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimMappings.
func (in *ClaimMappings) DeepCopy() *ClaimMappings {
if in == nil {
return nil
}
out := new(ClaimMappings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimValidationRule.
func (in *ClaimValidationRule) DeepCopy() *ClaimValidationRule {
if in == nil {
return nil
}
out := new(ClaimValidationRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Connection) DeepCopyInto(out *Connection) {
*out = *in
@ -148,6 +214,71 @@ func (in *EgressSelectorConfiguration) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Issuer) DeepCopyInto(out *Issuer) {
*out = *in
if in.Audiences != nil {
in, out := &in.Audiences, &out.Audiences
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Issuer.
func (in *Issuer) DeepCopy() *Issuer {
if in == nil {
return nil
}
out := new(Issuer)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) {
*out = *in
in.Issuer.DeepCopyInto(&out.Issuer)
if in.ClaimValidationRules != nil {
in, out := &in.ClaimValidationRules, &out.ClaimValidationRules
*out = make([]ClaimValidationRule, len(*in))
copy(*out, *in)
}
in.ClaimMappings.DeepCopyInto(&out.ClaimMappings)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator.
func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator {
if in == nil {
return nil
}
out := new(JWTAuthenticator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PrefixedClaimOrExpression) DeepCopyInto(out *PrefixedClaimOrExpression) {
*out = *in
if in.Prefix != nil {
in, out := &in.Prefix, &out.Prefix
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixedClaimOrExpression.
func (in *PrefixedClaimOrExpression) DeepCopy() *PrefixedClaimOrExpression {
if in == nil {
return nil
}
out := new(PrefixedClaimOrExpression)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPTransport) DeepCopyInto(out *TCPTransport) {
*out = *in

View File

@ -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
}

View File

@ -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 ""
}

View File

@ -78,6 +78,72 @@ func (in *AdmissionPluginConfiguration) DeepCopy() *AdmissionPluginConfiguration
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.JWT != nil {
in, out := &in.JWT, &out.JWT
*out = make([]JWTAuthenticator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfiguration.
func (in *AuthenticationConfiguration) DeepCopy() *AuthenticationConfiguration {
if in == nil {
return nil
}
out := new(AuthenticationConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AuthenticationConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) {
*out = *in
in.Username.DeepCopyInto(&out.Username)
in.Groups.DeepCopyInto(&out.Groups)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimMappings.
func (in *ClaimMappings) DeepCopy() *ClaimMappings {
if in == nil {
return nil
}
out := new(ClaimMappings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimValidationRule.
func (in *ClaimValidationRule) DeepCopy() *ClaimValidationRule {
if in == nil {
return nil
}
out := new(ClaimValidationRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Connection) DeepCopyInto(out *Connection) {
*out = *in
@ -148,6 +214,71 @@ func (in *EgressSelectorConfiguration) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Issuer) DeepCopyInto(out *Issuer) {
*out = *in
if in.Audiences != nil {
in, out := &in.Audiences, &out.Audiences
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Issuer.
func (in *Issuer) DeepCopy() *Issuer {
if in == nil {
return nil
}
out := new(Issuer)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) {
*out = *in
in.Issuer.DeepCopyInto(&out.Issuer)
if in.ClaimValidationRules != nil {
in, out := &in.ClaimValidationRules, &out.ClaimValidationRules
*out = make([]ClaimValidationRule, len(*in))
copy(*out, *in)
}
in.ClaimMappings.DeepCopyInto(&out.ClaimMappings)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator.
func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator {
if in == nil {
return nil
}
out := new(JWTAuthenticator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PrefixedClaimOrExpression) DeepCopyInto(out *PrefixedClaimOrExpression) {
*out = *in
if in.Prefix != nil {
in, out := &in.Prefix, &out.Prefix
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixedClaimOrExpression.
func (in *PrefixedClaimOrExpression) DeepCopy() *PrefixedClaimOrExpression {
if in == nil {
return nil
}
out := new(PrefixedClaimOrExpression)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPTransport) DeepCopyInto(out *TCPTransport) {
*out = *in

View File

@ -32,11 +32,9 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
@ -46,6 +44,8 @@ import (
"k8s.io/apimachinery/pkg/util/net"
"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/user"
certutil "k8s.io/client-go/util/cert"
@ -59,50 +59,17 @@ var (
)
type Options struct {
// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss"
// field of all tokens produced by the provider and is used for configuration
// 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
// JWTAuthenticator is the authenticator that will be used to verify the JWT.
JWTAuthenticator apiserver.JWTAuthenticator
// Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer.
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.
CAContentProvider CAContentProvider
// Optional http.Client used to make all requests to the remote issuer. Mutually exclusive with CAContentProvider.
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
// 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
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 func() time.Time
}
@ -192,13 +155,7 @@ func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier {
}
type Authenticator struct {
issuerURL string
usernameClaim string
usernamePrefix string
groupsClaim string
groupsPrefix string
requiredClaims map[string]string
jwtAuthenticator apiserver.JWTAuthenticator
// Contains an *oidc.IDTokenVerifier. Do not access directly use the
// idTokenVerifier method.
@ -240,19 +197,10 @@ var allowedSigningAlgs = map[string]bool{
}
func New(opts Options) (*Authenticator, error) {
url, err := url.Parse(opts.IssuerURL)
if err != nil {
if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil {
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
if len(supportedSigningAlgs) == 0 {
// 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 {
var roots *x509.CertPool
var err error
if opts.CAContentProvider != nil {
// TODO(enj): make this reload CA data dynamically
roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent())
@ -302,35 +251,30 @@ func New(opts Options) (*Authenticator, error) {
}
verifierConfig := &oidc.Config{
ClientID: opts.ClientID,
ClientID: opts.JWTAuthenticator.Issuer.Audiences[0],
SupportedSigningAlgs: supportedSigningAlgs,
Now: now,
}
var resolver *claimResolver
if opts.GroupsClaim != "" {
resolver = newClaimResolver(opts.GroupsClaim, client, verifierConfig)
if opts.JWTAuthenticator.ClaimMappings.Groups.Claim != "" {
resolver = newClaimResolver(opts.JWTAuthenticator.ClaimMappings.Groups.Claim, client, verifierConfig)
}
authenticator := &Authenticator{
issuerURL: opts.IssuerURL,
usernameClaim: opts.UsernameClaim,
usernamePrefix: opts.UsernamePrefix,
groupsClaim: opts.GroupsClaim,
groupsPrefix: opts.GroupsPrefix,
requiredClaims: opts.RequiredClaims,
cancel: cancel,
resolver: resolver,
jwtAuthenticator: opts.JWTAuthenticator,
cancel: cancel,
resolver: resolver,
}
if opts.KeySet != nil {
// 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 {
// Asynchronously attempt to initialize the authenticator. This enables
// self-hosted providers, providers that run on top of Kubernetes itself.
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 {
klog.Errorf("oidc authenticator: initializing plugin: %v", err)
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) {
if !hasCorrectIssuer(a.issuerURL, token) {
if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) {
return nil, false, nil
}
@ -577,11 +521,11 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
}
var username string
if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil {
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err)
if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Username.Claim, &username); err != nil {
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.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified {
@ -597,33 +541,36 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
}
}
if a.usernamePrefix != "" {
username = a.usernamePrefix + username
if a.jwtAuthenticator.ClaimMappings.Username.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Username.Prefix != "" {
username = *a.jwtAuthenticator.ClaimMappings.Username.Prefix + username
}
info := &user.DefaultInfo{Name: username}
if a.groupsClaim != "" {
if _, ok := c[a.groupsClaim]; ok {
if a.jwtAuthenticator.ClaimMappings.Groups.Claim != "" {
if _, ok := c[a.jwtAuthenticator.ClaimMappings.Groups.Claim]; ok {
// 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.
//
// See: https://github.com/kubernetes/kubernetes/issues/33290
var groups stringOrArray
if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil {
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err)
if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Groups.Claim, &groups); err != nil {
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.jwtAuthenticator.ClaimMappings.Groups.Claim, err)
}
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 {
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.
for claim, value := range a.requiredClaims {
for _, claimValidationRule := range a.jwtAuthenticator.ClaimValidationRules {
claim := claimValidationRule.Claim
value := claimValidationRule.RequiredValue
if !c.hasClaim(claim) {
return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim)
}

1
vendor/modules.txt vendored
View File

@ -1462,6 +1462,7 @@ k8s.io/apiserver/pkg/apis/apiserver/install
k8s.io/apiserver/pkg/apis/apiserver/v1
k8s.io/apiserver/pkg/apis/apiserver/v1alpha1
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/install
k8s.io/apiserver/pkg/apis/audit/v1