mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Merge pull request #119142 from aramase/aramase/f/kep_3331_add_feature_flag
[StructuredAuthenticationConfig] Add feature flag and wire up `--authentication-config` flag
This commit is contained in:
commit
f68c66f96d
@ -257,11 +257,8 @@ func TestAddFlags(t *testing.T) {
|
||||
RetryBackoff: apiserveroptions.DefaultAuthWebhookRetryBackoff(),
|
||||
},
|
||||
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
||||
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
},
|
||||
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
||||
OIDC: s.Authentication.OIDC,
|
||||
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
||||
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||
Lookup: true,
|
||||
ExtendExpiration: true,
|
||||
@ -327,7 +324,10 @@ func TestAddFlags(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
expected.Authentication.OIDC.UsernameClaim = "sub"
|
||||
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
|
||||
|
||||
if !reflect.DeepEqual(expected, s) {
|
||||
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{})))
|
||||
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{})))
|
||||
}
|
||||
}
|
||||
|
@ -243,11 +243,8 @@ func TestAddFlags(t *testing.T) {
|
||||
RetryBackoff: apiserveroptions.DefaultAuthWebhookRetryBackoff(),
|
||||
},
|
||||
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
||||
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
},
|
||||
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
||||
OIDC: s.Authentication.OIDC,
|
||||
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
||||
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||
Lookup: true,
|
||||
ExtendExpiration: true,
|
||||
@ -283,7 +280,10 @@ func TestAddFlags(t *testing.T) {
|
||||
AggregatorRejectForwardingRedirects: true,
|
||||
}
|
||||
|
||||
expected.Authentication.OIDC.UsernameClaim = "sub"
|
||||
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
|
||||
|
||||
if !reflect.DeepEqual(expected, s) {
|
||||
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{})))
|
||||
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{})))
|
||||
}
|
||||
}
|
||||
|
@ -27,14 +27,19 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver/install"
|
||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/egressselector"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
@ -47,6 +52,18 @@ import (
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const (
|
||||
oidcIssuerURLFlag = "oidc-issuer-url"
|
||||
oidcClientIDFlag = "oidc-client-id"
|
||||
oidcCAFileFlag = "oidc-ca-file"
|
||||
oidcUsernameClaimFlag = "oidc-username-claim"
|
||||
oidcUsernamePrefixFlag = "oidc-username-prefix"
|
||||
oidcGroupsClaimFlag = "oidc-groups-claim"
|
||||
oidcGroupsPrefixFlag = "oidc-groups-prefix"
|
||||
oidcSigningAlgsFlag = "oidc-signing-algs"
|
||||
oidcRequiredClaimFlag = "oidc-required-claim"
|
||||
)
|
||||
|
||||
// BuiltInAuthenticationOptions contains all build-in authentication options for API Server
|
||||
type BuiltInAuthenticationOptions struct {
|
||||
APIAudiences []string
|
||||
@ -59,6 +76,8 @@ type BuiltInAuthenticationOptions struct {
|
||||
TokenFile *TokenFileAuthenticationOptions
|
||||
WebHook *WebHookAuthenticationOptions
|
||||
|
||||
AuthenticationConfigFile string
|
||||
|
||||
TokenSuccessCacheTTL time.Duration
|
||||
TokenFailureCacheTTL time.Duration
|
||||
}
|
||||
@ -84,6 +103,9 @@ type OIDCAuthenticationOptions struct {
|
||||
GroupsPrefix string
|
||||
SigningAlgs []string
|
||||
RequiredClaims map[string]string
|
||||
|
||||
// areFlagsConfigured is a function that returns true if any of the oidc-* flags are configured.
|
||||
areFlagsConfigured func() bool
|
||||
}
|
||||
|
||||
// ServiceAccountAuthenticationOptions contains service account authentication options for API Server
|
||||
@ -154,7 +176,7 @@ func (o *BuiltInAuthenticationOptions) WithClientCert() *BuiltInAuthenticationOp
|
||||
|
||||
// WithOIDC set default value for OIDC authentication
|
||||
func (o *BuiltInAuthenticationOptions) WithOIDC() *BuiltInAuthenticationOptions {
|
||||
o.OIDC = &OIDCAuthenticationOptions{}
|
||||
o.OIDC = &OIDCAuthenticationOptions{areFlagsConfigured: func() bool { return false }}
|
||||
return o
|
||||
}
|
||||
|
||||
@ -190,9 +212,7 @@ func (o *BuiltInAuthenticationOptions) WithWebHook() *BuiltInAuthenticationOptio
|
||||
func (o *BuiltInAuthenticationOptions) Validate() []error {
|
||||
var allErrors []error
|
||||
|
||||
if o.OIDC != nil && (len(o.OIDC.IssuerURL) > 0) != (len(o.OIDC.ClientID) > 0) {
|
||||
allErrors = append(allErrors, fmt.Errorf("oidc-issuer-url and oidc-client-id should be specified together"))
|
||||
}
|
||||
allErrors = append(allErrors, o.validateOIDCOptions()...)
|
||||
|
||||
if o.ServiceAccounts != nil && len(o.ServiceAccounts.Issuers) > 0 {
|
||||
seen := make(map[string]bool)
|
||||
@ -274,45 +294,63 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
if o.OIDC != nil {
|
||||
fs.StringVar(&o.OIDC.IssuerURL, "oidc-issuer-url", o.OIDC.IssuerURL, ""+
|
||||
fs.StringVar(&o.OIDC.IssuerURL, oidcIssuerURLFlag, o.OIDC.IssuerURL, ""+
|
||||
"The URL of the OpenID issuer, only HTTPS scheme will be accepted. "+
|
||||
"If set, it will be used to verify the OIDC JSON Web Token (JWT).")
|
||||
|
||||
fs.StringVar(&o.OIDC.ClientID, "oidc-client-id", o.OIDC.ClientID,
|
||||
fs.StringVar(&o.OIDC.ClientID, oidcClientIDFlag, o.OIDC.ClientID,
|
||||
"The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set.")
|
||||
|
||||
fs.StringVar(&o.OIDC.CAFile, "oidc-ca-file", o.OIDC.CAFile, ""+
|
||||
fs.StringVar(&o.OIDC.CAFile, oidcCAFileFlag, o.OIDC.CAFile, ""+
|
||||
"If set, the OpenID server's certificate will be verified by one of the authorities "+
|
||||
"in the oidc-ca-file, otherwise the host's root CA set will be used.")
|
||||
|
||||
fs.StringVar(&o.OIDC.UsernameClaim, "oidc-username-claim", "sub", ""+
|
||||
fs.StringVar(&o.OIDC.UsernameClaim, oidcUsernameClaimFlag, "sub", ""+
|
||||
"The OpenID claim to use as the user name. Note that claims other than the default ('sub') "+
|
||||
"is not guaranteed to be unique and immutable. This flag is experimental, please see "+
|
||||
"the authentication documentation for further details.")
|
||||
|
||||
fs.StringVar(&o.OIDC.UsernamePrefix, "oidc-username-prefix", "", ""+
|
||||
fs.StringVar(&o.OIDC.UsernamePrefix, oidcUsernamePrefixFlag, "", ""+
|
||||
"If provided, all usernames will be prefixed with this value. If not provided, "+
|
||||
"username claims other than 'email' are prefixed by the issuer URL to avoid "+
|
||||
"clashes. To skip any prefixing, provide the value '-'.")
|
||||
|
||||
fs.StringVar(&o.OIDC.GroupsClaim, "oidc-groups-claim", "", ""+
|
||||
fs.StringVar(&o.OIDC.GroupsClaim, oidcGroupsClaimFlag, "", ""+
|
||||
"If provided, the name of a custom OpenID Connect claim for specifying user groups. "+
|
||||
"The claim value is expected to be a string or array of strings. This flag is experimental, "+
|
||||
"please see the authentication documentation for further details.")
|
||||
|
||||
fs.StringVar(&o.OIDC.GroupsPrefix, "oidc-groups-prefix", "", ""+
|
||||
fs.StringVar(&o.OIDC.GroupsPrefix, oidcGroupsPrefixFlag, "", ""+
|
||||
"If provided, all groups will be prefixed with this value to prevent conflicts with "+
|
||||
"other authentication strategies.")
|
||||
|
||||
fs.StringSliceVar(&o.OIDC.SigningAlgs, "oidc-signing-algs", []string{"RS256"}, ""+
|
||||
fs.StringSliceVar(&o.OIDC.SigningAlgs, oidcSigningAlgsFlag, []string{"RS256"}, ""+
|
||||
"Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a "+
|
||||
"supported 'alg' header values are: RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512. "+
|
||||
"Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1.")
|
||||
|
||||
fs.Var(cliflag.NewMapStringStringNoSplit(&o.OIDC.RequiredClaims), "oidc-required-claim", ""+
|
||||
fs.Var(cliflag.NewMapStringStringNoSplit(&o.OIDC.RequiredClaims), oidcRequiredClaimFlag, ""+
|
||||
"A key=value pair that describes a required claim in the ID Token. "+
|
||||
"If set, the claim is verified to be present in the ID Token with a matching value. "+
|
||||
"Repeat this flag to specify multiple claims.")
|
||||
|
||||
fs.StringVar(&o.AuthenticationConfigFile, "authentication-config", o.AuthenticationConfigFile, ""+
|
||||
"File with Authentication Configuration to configure the JWT Token authenticator. "+
|
||||
"Note: This feature is in Alpha since v1.29."+
|
||||
"--feature-gate=StructuredAuthenticationConfiguration=true needs to be set for enabling this feature."+
|
||||
"This feature is mutually exclusive with the oidc-* flags.")
|
||||
|
||||
o.OIDC.areFlagsConfigured = func() bool {
|
||||
return fs.Changed(oidcIssuerURLFlag) ||
|
||||
fs.Changed(oidcClientIDFlag) ||
|
||||
fs.Changed(oidcCAFileFlag) ||
|
||||
fs.Changed(oidcUsernameClaimFlag) ||
|
||||
fs.Changed(oidcUsernamePrefixFlag) ||
|
||||
fs.Changed(oidcGroupsClaimFlag) ||
|
||||
fs.Changed(oidcGroupsPrefixFlag) ||
|
||||
fs.Changed(oidcSigningAlgsFlag) ||
|
||||
fs.Changed(oidcRequiredClaimFlag)
|
||||
}
|
||||
}
|
||||
|
||||
if o.RequestHeader != nil {
|
||||
@ -401,7 +439,14 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
|
||||
}
|
||||
}
|
||||
|
||||
if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 {
|
||||
// When the StructuredAuthenticationConfiguration feature is enabled and the authentication config file is provided,
|
||||
// load the authentication config from the file.
|
||||
if len(o.AuthenticationConfigFile) > 0 {
|
||||
var err error
|
||||
if ret.AuthenticationConfig, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil {
|
||||
return kubeauthenticator.Config{}, err
|
||||
}
|
||||
} else 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" {
|
||||
@ -458,13 +503,17 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
|
||||
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
|
||||
}
|
||||
|
||||
if ret.AuthenticationConfig != nil {
|
||||
if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig).ToAggregate(); err != nil {
|
||||
return kubeauthenticator.Config{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if o.RequestHeader != nil {
|
||||
var err error
|
||||
ret.RequestHeaderConfig, err = o.RequestHeader.ToAuthenticationRequestHeaderConfig()
|
||||
@ -584,3 +633,62 @@ func (o *BuiltInAuthenticationOptions) ApplyAuthorization(authorization *BuiltIn
|
||||
o.Anonymous.Allow = false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *BuiltInAuthenticationOptions) validateOIDCOptions() []error {
|
||||
var allErrors []error
|
||||
|
||||
// Existing validation when jwt authenticator is configured with oidc-* flags
|
||||
if len(o.AuthenticationConfigFile) == 0 {
|
||||
if o.OIDC != nil && o.OIDC.areFlagsConfigured() && (len(o.OIDC.IssuerURL) == 0 || len(o.OIDC.ClientID) == 0) {
|
||||
allErrors = append(allErrors, fmt.Errorf("oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set"))
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// New validation when authentication config file is provided
|
||||
|
||||
// Authentication config file is only supported when the StructuredAuthenticationConfiguration feature is enabled
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthenticationConfiguration) {
|
||||
allErrors = append(allErrors, fmt.Errorf("set --feature-gates=%s=true to use authentication-config file", genericfeatures.StructuredAuthenticationConfiguration))
|
||||
}
|
||||
|
||||
// Authentication config file and oidc-* flags are mutually exclusive
|
||||
if o.OIDC != nil && o.OIDC.areFlagsConfigured() {
|
||||
allErrors = append(allErrors, fmt.Errorf("authentication-config file and oidc-* flags are mutually exclusive"))
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
var (
|
||||
cfgScheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(cfgScheme, serializer.EnableStrict)
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(cfgScheme)
|
||||
}
|
||||
|
||||
// loadAuthenticationConfig parses the authentication configuration from the given file and returns it.
|
||||
func loadAuthenticationConfig(configFilePath string) (*apiserver.AuthenticationConfiguration, error) {
|
||||
// read from file
|
||||
data, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty config file %q", configFilePath)
|
||||
}
|
||||
|
||||
decodedObj, err := runtime.Decode(codecs.UniversalDecoder(), data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuration, ok := decodedObj.(*apiserver.AuthenticationConfiguration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected AuthenticationConfiguration, got %T", decodedObj)
|
||||
}
|
||||
|
||||
return configuration, nil
|
||||
}
|
||||
|
@ -32,18 +32,22 @@ import (
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
|
||||
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
apiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestAuthenticationValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
testOIDC *OIDCAuthenticationOptions
|
||||
testSA *ServiceAccountAuthenticationOptions
|
||||
testWebHook *WebHookAuthenticationOptions
|
||||
expectErr string
|
||||
name string
|
||||
testOIDC *OIDCAuthenticationOptions
|
||||
testSA *ServiceAccountAuthenticationOptions
|
||||
testWebHook *WebHookAuthenticationOptions
|
||||
testAuthenticationConfigFile string
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "test when OIDC and ServiceAccounts are nil",
|
||||
@ -51,10 +55,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when OIDC and ServiceAccounts are valid",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{"http://foo.bar.com"},
|
||||
@ -64,23 +69,25 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when OIDC is invalid",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{"http://foo.bar.com"},
|
||||
KeyFiles: []string{"testkeyfile1", "testkeyfile2"},
|
||||
},
|
||||
expectErr: "oidc-issuer-url and oidc-client-id should be specified together",
|
||||
expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
|
||||
},
|
||||
{
|
||||
name: "test when ServiceAccounts doesn't have key file",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{"http://foo.bar.com"},
|
||||
@ -90,10 +97,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccounts doesn't have issuer",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{},
|
||||
@ -103,10 +111,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccounts has empty string as issuer",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{""},
|
||||
@ -116,10 +125,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccounts has duplicate issuers",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{"http://foo.bar.com", "http://foo.bar.com"},
|
||||
@ -129,10 +139,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccount has bad issuer",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
Issuers: []string{"http://[::1]:namedport"},
|
||||
@ -142,10 +153,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccounts has invalid JWKSURI",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
KeyFiles: []string{"cert", "key"},
|
||||
@ -157,10 +169,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when ServiceAccounts has invalid JWKSURI (not https scheme)",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
KeyFiles: []string{"cert", "key"},
|
||||
@ -172,10 +185,11 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
{
|
||||
name: "test when WebHook has invalid retry attempts",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
testSA: &ServiceAccountAuthenticationOptions{
|
||||
KeyFiles: []string{"cert", "key"},
|
||||
@ -195,6 +209,23 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
},
|
||||
expectErr: "number of webhook retry attempts must be greater than 0, but is: 0",
|
||||
},
|
||||
{
|
||||
name: "test when authentication config file is set without feature gate",
|
||||
testAuthenticationConfigFile: "configfile",
|
||||
expectErr: "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file",
|
||||
},
|
||||
{
|
||||
name: "test when authentication config file and oidc-* flags are set",
|
||||
testAuthenticationConfigFile: "configfile",
|
||||
testOIDC: &OIDCAuthenticationOptions{
|
||||
UsernameClaim: "sub",
|
||||
SigningAlgs: []string{"RS256"},
|
||||
IssuerURL: "https://testIssuerURL",
|
||||
ClientID: "testClientID",
|
||||
areFlagsConfigured: func() bool { return true },
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testCases {
|
||||
@ -203,6 +234,7 @@ func TestAuthenticationValidate(t *testing.T) {
|
||||
options.OIDC = testcase.testOIDC
|
||||
options.ServiceAccounts = testcase.testSA
|
||||
options.WebHook = testcase.testWebHook
|
||||
options.AuthenticationConfigFile = testcase.testAuthenticationConfigFile
|
||||
|
||||
errs := options.Validate()
|
||||
if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), testcase.expectErr) || testcase.expectErr == "") {
|
||||
@ -402,8 +434,14 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !opts.OIDC.areFlagsConfigured() {
|
||||
t.Fatal("OIDC flags should be configured")
|
||||
}
|
||||
// nil these out because you cannot compare functions
|
||||
opts.OIDC.areFlagsConfigured = nil
|
||||
|
||||
if !reflect.DeepEqual(opts, expected) {
|
||||
t.Error(cmp.Diff(opts, expected))
|
||||
t.Error(cmp.Diff(opts, expected, cmp.AllowUnexported(OIDCAuthenticationOptions{})))
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,3 +662,355 @@ func TestToAuthenticationConfig_OIDC(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOIDCOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
structuredAuthenticationConfigEnabled bool
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "issuer url and client id are not set",
|
||||
args: []string{
|
||||
"--oidc-username-claim=testClaim",
|
||||
},
|
||||
expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
|
||||
},
|
||||
{
|
||||
name: "issuer url set, client id is not set",
|
||||
args: []string{
|
||||
"--oidc-issuer-url=https://testIssuerURL",
|
||||
"--oidc-username-claim=testClaim",
|
||||
},
|
||||
expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
|
||||
},
|
||||
{
|
||||
name: "issuer url is not set, client id is set",
|
||||
args: []string{
|
||||
"--oidc-client-id=testClientID",
|
||||
"--oidc-username-claim=testClaim",
|
||||
},
|
||||
expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
|
||||
},
|
||||
{
|
||||
name: "issuer url and client id are set",
|
||||
args: []string{
|
||||
"--oidc-client-id=testClientID",
|
||||
"--oidc-issuer-url=https://testIssuerURL",
|
||||
},
|
||||
expectErr: "",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, feature gate is not enabled",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
},
|
||||
expectErr: "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-issuer-url is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-issuer-url=https://testIssuerURL",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-client-id is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-client-id=testClientID",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-username-claim is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-username-claim=testClaim",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-username-prefix is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-username-prefix=testPrefix",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-ca-file is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-ca-file=testCAFile",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-groups-claim is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-groups-claim=testClaim",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-groups-prefix is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-groups-prefix=testPrefix",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-required-claim is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-required-claim=foo=bar",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-signature-algs is set",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-signing-algs=RS512",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-username-claim flag not set, defaulting shouldn't error",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
},
|
||||
expectErr: "",
|
||||
structuredAuthenticationConfigEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "authentication-config file, --oidc-username-claim flag explicitly set with default value should error",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
"--oidc-username-claim=sub",
|
||||
},
|
||||
expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "valid authentication-config file",
|
||||
args: []string{
|
||||
"--authentication-config=configfile",
|
||||
},
|
||||
structuredAuthenticationConfigEnabled: true,
|
||||
expectErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, tt.structuredAuthenticationConfigEnabled)()
|
||||
|
||||
opts := NewBuiltInAuthenticationOptions().WithOIDC()
|
||||
pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError)
|
||||
opts.AddFlags(pf)
|
||||
|
||||
if err := pf.Parse(tt.args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
errs := opts.Validate()
|
||||
if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), tt.expectErr) || tt.expectErr == "") {
|
||||
t.Errorf("Got err: %v, Expected err: %s", errs, tt.expectErr)
|
||||
}
|
||||
if len(errs) == 0 && len(tt.expectErr) != 0 {
|
||||
t.Errorf("Got err nil, Expected err: %s", tt.expectErr)
|
||||
}
|
||||
if len(errs) > 0 && len(tt.expectErr) == 0 {
|
||||
t.Errorf("Got err: %v, Expected err nil", errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAuthenticationConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
file func() string
|
||||
expectErr string
|
||||
expectedConfig *apiserver.AuthenticationConfiguration
|
||||
}{
|
||||
{
|
||||
name: "empty file",
|
||||
file: func() string { return writeTempFile(t, ``) },
|
||||
expectErr: "empty config file",
|
||||
expectedConfig: nil,
|
||||
},
|
||||
{
|
||||
name: "valid file",
|
||||
file: func() string {
|
||||
return writeTempFile(t,
|
||||
`{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
|
||||
"kind":"AuthenticationConfiguration",
|
||||
"jwt":[{"issuer":{"url": "https://test-issuer"}}]}`)
|
||||
},
|
||||
expectErr: "",
|
||||
expectedConfig: &apiserver.AuthenticationConfiguration{
|
||||
JWT: []apiserver.JWTAuthenticator{
|
||||
{
|
||||
Issuer: apiserver.Issuer{URL: "https://test-issuer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing file",
|
||||
file: func() string { return "bogus-missing-file" },
|
||||
expectErr: "no such file or directory",
|
||||
expectedConfig: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid content file",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration","authorizers":{"type":"Webhook"}}`)
|
||||
},
|
||||
expectErr: `no kind "AuthenticationConfiguration" is registered for version "apiserver.config.k8s.io/v99"`,
|
||||
expectedConfig: nil,
|
||||
},
|
||||
{
|
||||
name: "missing apiVersion",
|
||||
file: func() string { return writeTempFile(t, `{"kind":"AuthenticationConfiguration"}`) },
|
||||
expectErr: `'apiVersion' is missing`,
|
||||
},
|
||||
{
|
||||
name: "missing kind",
|
||||
file: func() string { return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1"}`) },
|
||||
expectErr: `'Kind' is missing`,
|
||||
},
|
||||
{
|
||||
name: "unknown group",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{"apiVersion":"apps/v1alpha1","kind":"AuthenticationConfiguration"}`)
|
||||
},
|
||||
expectErr: `apps/v1alpha1`,
|
||||
},
|
||||
{
|
||||
name: "unknown version",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration"}`)
|
||||
},
|
||||
expectErr: `apiserver.config.k8s.io/v99`,
|
||||
},
|
||||
{
|
||||
name: "unknown kind",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1","kind":"SomeConfiguration"}`)
|
||||
},
|
||||
expectErr: `SomeConfiguration`,
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
|
||||
"kind":"AuthenticationConfiguration",
|
||||
"jwt1":[{"issuer":{"url": "https://test-issuer"}}]}`)
|
||||
},
|
||||
expectErr: `unknown field "jwt1"`,
|
||||
},
|
||||
{
|
||||
name: "v1alpha1 - json",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
|
||||
"kind":"AuthenticationConfiguration",
|
||||
"jwt":[{"issuer":{"url": "https://test-issuer"}}]}`)
|
||||
},
|
||||
expectedConfig: &apiserver.AuthenticationConfiguration{
|
||||
JWT: []apiserver.JWTAuthenticator{
|
||||
{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://test-issuer",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1alpha1 - yaml",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: https://test-issuer
|
||||
claimMappings:
|
||||
username:
|
||||
claim: sub
|
||||
prefix: ""
|
||||
`)
|
||||
},
|
||||
expectedConfig: &apiserver.AuthenticationConfiguration{
|
||||
JWT: []apiserver.JWTAuthenticator{
|
||||
{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://test-issuer",
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "sub",
|
||||
Prefix: pointer.String(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1alpha1 - no jwt",
|
||||
file: func() string {
|
||||
return writeTempFile(t, `{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
|
||||
"kind":"AuthenticationConfiguration"}`)
|
||||
},
|
||||
expectedConfig: &apiserver.AuthenticationConfiguration{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config, err := loadAuthenticationConfig(tc.file())
|
||||
if !strings.Contains(errString(err), tc.expectErr) {
|
||||
t.Fatalf("expected error %q, got %v", tc.expectErr, err)
|
||||
}
|
||||
if !reflect.DeepEqual(config, tc.expectedConfig) {
|
||||
t.Fatalf("unexpected config:\n%s", cmp.Diff(tc.expectedConfig, config))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
file, err := os.CreateTemp("", "config")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(file.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
)
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&AdmissionConfiguration{},
|
||||
&AuthenticationConfiguration{},
|
||||
&EgressSelectorConfiguration{},
|
||||
&TracingConfiguration{},
|
||||
)
|
||||
|
@ -53,6 +53,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
&EgressSelectorConfiguration{},
|
||||
)
|
||||
scheme.AddKnownTypes(ConfigSchemeGroupVersion,
|
||||
&AuthenticationConfiguration{},
|
||||
&TracingConfiguration{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
|
@ -198,6 +198,13 @@ const (
|
||||
// document.
|
||||
StorageVersionHash featuregate.Feature = "StorageVersionHash"
|
||||
|
||||
// owner: @aramase, @enj, @nabokihms
|
||||
// kep: https://kep.k8s.io/3331
|
||||
// alpha: v1.29
|
||||
//
|
||||
// Enables Structured Authentication Configuration
|
||||
StructuredAuthenticationConfiguration featuregate.Feature = "StructuredAuthenticationConfiguration"
|
||||
|
||||
// owner: @wojtek-t
|
||||
// alpha: v1.15
|
||||
// beta: v1.16
|
||||
@ -278,6 +285,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
StorageVersionHash: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
StructuredAuthenticationConfiguration: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
WatchBookmark: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
|
||||
|
||||
InPlacePodVerticalScaling: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
@ -27,23 +27,28 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
|
||||
utilsnet "k8s.io/utils/net"
|
||||
@ -95,9 +100,21 @@ var (
|
||||
)
|
||||
|
||||
func TestOIDC(t *testing.T) {
|
||||
t.Log("Testing OIDC authenticator with --oidc-* flags")
|
||||
runTests(t, false)
|
||||
}
|
||||
|
||||
func TestStructuredAuthenticationConfig(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
|
||||
|
||||
t.Log("Testing OIDC authenticator with authentication config")
|
||||
runTests(t, true)
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
configureInfrastructure func(t *testing.T) (
|
||||
configureInfrastructure func(t *testing.T, useAuthenticationConfig bool) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@ -207,7 +224,7 @@ func TestOIDC(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "ID token signature can not be verified due to wrong JWKs",
|
||||
configureInfrastructure: func(t *testing.T) (
|
||||
configureInfrastructure: func(t *testing.T, useAuthenticationConfig bool) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@ -220,7 +237,13 @@ func TestOIDC(t *testing.T) {
|
||||
require.NoError(t, wantErr)
|
||||
|
||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath)
|
||||
|
||||
if useAuthenticationConfig {
|
||||
authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix)
|
||||
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
|
||||
} else {
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
|
||||
}
|
||||
|
||||
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
|
||||
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
|
||||
@ -252,7 +275,7 @@ func TestOIDC(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t)
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, useAuthenticationConfig)
|
||||
|
||||
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
|
||||
|
||||
@ -304,7 +327,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t)
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, false)
|
||||
|
||||
tokenURL, err := oidcServer.TokenURL()
|
||||
require.NoError(t, err)
|
||||
@ -333,7 +356,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func configureTestInfrastructure(t *testing.T) (
|
||||
func configureTestInfrastructure(t *testing.T, useAuthenticationConfig bool) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@ -348,7 +371,13 @@ func configureTestInfrastructure(t *testing.T) (
|
||||
require.NoError(t, err)
|
||||
|
||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath)
|
||||
|
||||
if useAuthenticationConfig {
|
||||
authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix)
|
||||
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
|
||||
} else {
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
|
||||
}
|
||||
|
||||
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &signingPrivateKey.PublicKey))
|
||||
|
||||
@ -399,19 +428,26 @@ func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, c
|
||||
return cfg
|
||||
}
|
||||
|
||||
func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath string) *kubeapiserverapptesting.TestServer {
|
||||
func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string) *kubeapiserverapptesting.TestServer {
|
||||
t.Helper()
|
||||
|
||||
server, err := kubeapiserverapptesting.StartTestServer(
|
||||
t,
|
||||
kubeapiserverapptesting.NewDefaultTestServerOptions(),
|
||||
[]string{
|
||||
var customFlags []string
|
||||
if authenticationConfigYAML != "" {
|
||||
customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, authenticationConfigYAML))}
|
||||
} else {
|
||||
customFlags = []string{
|
||||
fmt.Sprintf("--oidc-issuer-url=%s", oidcURL),
|
||||
fmt.Sprintf("--oidc-client-id=%s", oidcClientID),
|
||||
fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath),
|
||||
fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix),
|
||||
fmt.Sprintf("--authorization-mode=%s", modes.ModeRBAC),
|
||||
},
|
||||
}
|
||||
}
|
||||
customFlags = append(customFlags, "--authorization-mode=RBAC")
|
||||
|
||||
server, err := kubeapiserverapptesting.StartTestServer(
|
||||
t,
|
||||
kubeapiserverapptesting.NewDefaultTestServerOptions(),
|
||||
customFlags,
|
||||
framework.SharedEtcd(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@ -494,3 +530,44 @@ func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath str
|
||||
|
||||
return cert, key, certFilePath, keyFilePath
|
||||
}
|
||||
|
||||
func writeTempFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
file, err := os.CreateTemp("", "oidc-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(file.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func generateAuthenticationConfig(t *testing.T, issuerURL, clientID, caCert, usernamePrefix string) string {
|
||||
t.Helper()
|
||||
|
||||
// Indent the certificate authority to match the format of the generated
|
||||
// authentication config.
|
||||
caCert = strings.ReplaceAll(caCert, "\n", "\n ")
|
||||
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
claim: sub
|
||||
prefix: %s
|
||||
`, issuerURL, clientID, string(caCert), usernamePrefix)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user