Add --authorization-config flag to apiserver

Signed-off-by: Nabarun Pal <pal.nabarun95@gmail.com>
This commit is contained in:
Nabarun Pal 2023-09-25 09:18:11 +05:30
parent 007ef653ad
commit 22e5a806a7
No known key found for this signature in database
GPG Key ID: E71158161DF2A2CB
7 changed files with 532 additions and 11 deletions

View File

@ -327,6 +327,13 @@ func TestAddFlags(t *testing.T) {
expected.Authentication.OIDC.UsernameClaim = "sub"
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
if !s.Authorization.AreLegacyFlagsSet() {
t.Errorf("expected legacy authorization flags to be set")
}
// setting the method to nil since methods can't be compared with reflect.DeepEqual
s.Authorization.AreLegacyFlagsSet = nil
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{}, kubeoptions.OIDCAuthenticationOptions{})))
}

View File

@ -283,6 +283,12 @@ func TestAddFlags(t *testing.T) {
expected.Authentication.OIDC.UsernameClaim = "sub"
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
if !s.Authorization.AreLegacyFlagsSet() {
t.Errorf("expected legacy authorization flags to be set")
}
// setting the method to nil since methods can't be compared with reflect.DeepEqual
s.Authorization.AreLegacyFlagsSet = nil
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{}, kubeoptions.OIDCAuthenticationOptions{})))
}

View File

@ -21,12 +21,17 @@ import (
"strings"
"time"
"k8s.io/apiserver/pkg/apis/apiserver/load"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/apis/apiserver/validation"
genericoptions "k8s.io/apiserver/pkg/server/options"
versionedinformers "k8s.io/client-go/informers"
@ -35,9 +40,19 @@ import (
)
const (
defaultWebhookName = "default"
defaultWebhookName = "default"
authorizationModeFlag = "authorization-mode"
authorizationWebhookConfigFileFlag = "authorization-webhook-config-file"
authorizationWebhookVersionFlag = "authorization-webhook-version"
authorizationWebhookAuthorizedTTLFlag = "authorization-webhook-cache-authorized-ttl"
authorizationWebhookUnauthorizedTTLFlag = "authorization-webhook-cache-unauthorized-ttl"
authorizationPolicyFileFlag = "authorization-policy-file"
authorizationConfigFlag = "authorization-config"
)
// RepeatableAuthorizerTypes is the list of Authorizer that can be repeated in the Authorization Config
var repeatableAuthorizerTypes = []string{authzmodes.ModeWebhook}
// BuiltInAuthorizationOptions contains all build-in authorization options for API Server
type BuiltInAuthorizationOptions struct {
Modes []string
@ -50,6 +65,16 @@ type BuiltInAuthorizationOptions struct {
// This allows us to configure the sleep time at each iteration and the maximum number of retries allowed
// before we fail the webhook call in order to limit the fan out that ensues when the system is degraded.
WebhookRetryBackoff *wait.Backoff
// AuthorizationConfigurationFile is mutually exclusive with all of:
// - Modes
// - WebhookConfigFile
// - WebHookVersion
// - WebhookCacheAuthorizedTTL
// - WebhookCacheUnauthorizedTTL
AuthorizationConfigurationFile string
AreLegacyFlagsSet func() bool
}
// NewBuiltInAuthorizationOptions create a BuiltInAuthorizationOptions with default value
@ -69,6 +94,54 @@ func (o *BuiltInAuthorizationOptions) Validate() []error {
return nil
}
var allErrors []error
// if --authorization-config is set, check if
// - the feature flag is set
// - legacyFlags are not set
// - the config file can be loaded
// - the config file represents a valid configuration
if o.AuthorizationConfigurationFile != "" {
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
return append(allErrors, fmt.Errorf("--%s cannot be used without enabling StructuredAuthorizationConfiguration feature flag", authorizationConfigFlag))
}
// error out if legacy flags are defined
if o.AreLegacyFlagsSet != nil && o.AreLegacyFlagsSet() {
return append(allErrors, fmt.Errorf("--%s can not be specified when --%s or --authorization-webhook-* flags are defined", authorizationConfigFlag, authorizationModeFlag))
}
// load the file and check for errors
config, err := load.LoadFromFile(o.AuthorizationConfigurationFile)
if err != nil {
return append(allErrors, fmt.Errorf("failed to load AuthorizationConfiguration from file: %v", err))
}
// validate the file and return any error
if errors := validation.ValidateAuthorizationConfiguration(nil, config,
sets.NewString(authzmodes.AuthorizationModeChoices...),
sets.NewString(repeatableAuthorizerTypes...),
); len(errors) != 0 {
allErrors = append(allErrors, errors.ToAggregate().Errors()...)
}
// test to check if the authorizer names passed conform to the authorizers for type!=Webhook
// this test is only for kube-apiserver and hence checked here
// it preserves compatibility with o.buildAuthorizationConfiguration
for _, authorizer := range config.Authorizers {
if string(authorizer.Type) == authzmodes.ModeWebhook {
continue
}
expectedName := getNameForAuthorizerMode(string(authorizer.Type))
if expectedName != authorizer.Name {
allErrors = append(allErrors, fmt.Errorf("expected name %s for authorizer %s instead of %s", expectedName, authorizer.Type, authorizer.Name))
}
}
return allErrors
}
// validate the legacy flags using the legacy mode if --authorization-config is not passed
if len(o.Modes) == 0 {
allErrors = append(allErrors, fmt.Errorf("at least one authorization-mode must be passed"))
}
@ -111,27 +184,47 @@ func (o *BuiltInAuthorizationOptions) AddFlags(fs *pflag.FlagSet) {
return
}
fs.StringSliceVar(&o.Modes, "authorization-mode", o.Modes, ""+
fs.StringSliceVar(&o.Modes, authorizationModeFlag, o.Modes, ""+
"Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+
strings.Join(authzmodes.AuthorizationModeChoices, ",")+".")
fs.StringVar(&o.PolicyFile, "authorization-policy-file", o.PolicyFile, ""+
fs.StringVar(&o.PolicyFile, authorizationPolicyFileFlag, o.PolicyFile, ""+
"File with authorization policy in json line by line format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&o.WebhookConfigFile, "authorization-webhook-config-file", o.WebhookConfigFile, ""+
fs.StringVar(&o.WebhookConfigFile, authorizationWebhookConfigFileFlag, o.WebhookConfigFile, ""+
"File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. "+
"The API server will query the remote service to determine access on the API server's secure port.")
fs.StringVar(&o.WebhookVersion, "authorization-webhook-version", o.WebhookVersion, ""+
fs.StringVar(&o.WebhookVersion, authorizationWebhookVersionFlag, o.WebhookVersion, ""+
"The API version of the authorization.k8s.io SubjectAccessReview to send to and expect from the webhook.")
fs.DurationVar(&o.WebhookCacheAuthorizedTTL, "authorization-webhook-cache-authorized-ttl",
fs.DurationVar(&o.WebhookCacheAuthorizedTTL, authorizationWebhookAuthorizedTTLFlag,
o.WebhookCacheAuthorizedTTL,
"The duration to cache 'authorized' responses from the webhook authorizer.")
fs.DurationVar(&o.WebhookCacheUnauthorizedTTL,
"authorization-webhook-cache-unauthorized-ttl", o.WebhookCacheUnauthorizedTTL,
authorizationWebhookUnauthorizedTTLFlag, o.WebhookCacheUnauthorizedTTL,
"The duration to cache 'unauthorized' responses from the webhook authorizer.")
fs.StringVar(&o.AuthorizationConfigurationFile, authorizationConfigFlag, o.AuthorizationConfigurationFile, ""+
"File with Authorization Configuration to configure the authorizer chain."+
"Note: This feature is in Alpha since v1.29."+
"--feature-gate=StructuredAuthorizationConfiguration=true feature flag needs to be set to true for enabling the functionality."+
"This feature is mutually exclusive with the other --authorization-mode and --authorization-webhook-* flags.")
// preserves compatibility with any method set during initialization
oldAreLegacyFlagsSet := o.AreLegacyFlagsSet
o.AreLegacyFlagsSet = func() bool {
if oldAreLegacyFlagsSet != nil && oldAreLegacyFlagsSet() {
return true
}
return fs.Changed(authorizationModeFlag) ||
fs.Changed(authorizationWebhookConfigFileFlag) ||
fs.Changed(authorizationWebhookVersionFlag) ||
fs.Changed(authorizationWebhookAuthorizedTTLFlag) ||
fs.Changed(authorizationWebhookUnauthorizedTTLFlag)
}
}
// ToAuthorizationConfig convert BuiltInAuthorizationOptions to authorizer.Config
@ -140,9 +233,44 @@ func (o *BuiltInAuthorizationOptions) ToAuthorizationConfig(versionedInformerFac
return nil, nil
}
authzConfiguration, err := o.buildAuthorizationConfiguration()
if err != nil {
return nil, fmt.Errorf("failed to build authorization config: %s", err)
var authorizationConfiguration *authzconfig.AuthorizationConfiguration
var err error
// if --authorization-config is set, check if
// - the feature flag is set
// - legacyFlags are not set
// - the config file can be loaded
// - the config file represents a valid configuration
// else,
// - build the AuthorizationConfig from the legacy flags
if o.AuthorizationConfigurationFile != "" {
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
return nil, fmt.Errorf("--%s cannot be used without enabling StructuredAuthorizationConfiguration feature flag", authorizationConfigFlag)
}
// error out if legacy flags are defined
if o.AreLegacyFlagsSet != nil && o.AreLegacyFlagsSet() {
return nil, fmt.Errorf("--%s can not be specified when --%s or --authorization-webhook-* flags are defined", authorizationConfigFlag, authorizationModeFlag)
}
// load the file and check for errors
authorizationConfiguration, err = load.LoadFromFile(o.AuthorizationConfigurationFile)
if err != nil {
return nil, fmt.Errorf("failed to load AuthorizationConfiguration from file: %v", err)
}
// validate the file and return any error
if errors := validation.ValidateAuthorizationConfiguration(nil, authorizationConfiguration,
sets.NewString(authzmodes.AuthorizationModeChoices...),
sets.NewString(repeatableAuthorizerTypes...),
); len(errors) != 0 {
return nil, fmt.Errorf(errors.ToAggregate().Error())
}
} else {
authorizationConfiguration, err = o.buildAuthorizationConfiguration()
if err != nil {
return nil, fmt.Errorf("failed to build authorization config: %s", err)
}
}
return &authorizer.Config{
@ -150,7 +278,7 @@ func (o *BuiltInAuthorizationOptions) ToAuthorizationConfig(versionedInformerFac
VersionedInformerFactory: versionedInformerFactory,
WebhookRetryBackoff: o.WebhookRetryBackoff,
AuthorizationConfiguration: authzConfiguration,
AuthorizationConfiguration: authorizationConfiguration,
}, nil
}

View File

@ -173,6 +173,13 @@ func TestBuiltInAuthorizationOptionsAddFlags(t *testing.T) {
t.Fatal(err)
}
if !opts.AreLegacyFlagsSet() {
t.Fatal("legacy flags should have been configured")
}
// setting the method to nil since methods can't be compared with reflect.DeepEqual
opts.AreLegacyFlagsSet = nil
if !reflect.DeepEqual(opts, expected) {
t.Error(cmp.Diff(opts, expected))
}

View File

@ -0,0 +1,82 @@
/*
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 load
import (
"fmt"
"io"
"os"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
api "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/apis/apiserver/install"
externalapi "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
)
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict)
)
func init() {
install.Install(scheme)
}
func LoadFromFile(file string) (*api.AuthorizationConfiguration, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return LoadFromData(data)
}
func LoadFromReader(reader io.Reader) (*api.AuthorizationConfiguration, error) {
if reader == nil {
// no reader specified, use default config
return LoadFromData(nil)
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return LoadFromData(data)
}
func LoadFromData(data []byte) (*api.AuthorizationConfiguration, error) {
if len(data) == 0 {
// no config provided, return default
externalConfig := &externalapi.AuthorizationConfiguration{}
scheme.Default(externalConfig)
internalConfig := &api.AuthorizationConfiguration{}
if err := scheme.Convert(externalConfig, internalConfig, nil); err != nil {
return nil, err
}
return internalConfig, nil
}
decodedObj, err := runtime.Decode(codecs.UniversalDecoder(), data)
if err != nil {
return nil, err
}
configuration, ok := decodedObj.(*api.AuthorizationConfiguration)
if !ok {
return nil, fmt.Errorf("expected AuthorizationConfiguration, got %T", decodedObj)
}
return configuration, nil
}

View File

@ -0,0 +1,290 @@
/*
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 load
import (
"bytes"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "k8s.io/apiserver/pkg/apis/apiserver"
)
var defaultConfig = &api.AuthorizationConfiguration{}
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 TestLoadFromFile(t *testing.T) {
// no file
{
_, err := LoadFromFile("")
if err == nil {
t.Fatalf("expected err: %v", err)
}
}
// empty file
{
config, err := LoadFromFile(writeTempFile(t, ``))
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(config, defaultConfig) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(defaultConfig, config))
}
}
// valid file
{
input := `{
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
"kind":"AuthorizationConfiguration",
"authorizers":[{"type":"Webhook"}]}`
expect := &api.AuthorizationConfiguration{
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
}
config, err := LoadFromFile(writeTempFile(t, input))
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(config, expect) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(expect, config))
}
}
// missing file
{
_, err := LoadFromFile(`bogus-missing-file`)
if err == nil {
t.Fatalf("expected err, got none")
}
if !strings.Contains(err.Error(), "bogus-missing-file") {
t.Fatalf("expected missing file error, got %v", err)
}
}
// invalid content file
{
input := `{
"apiVersion":"apiserver.config.k8s.io/v99",
"kind":"AuthorizationConfiguration",
"authorizers":{"type":"Webhook"}}`
_, err := LoadFromFile(writeTempFile(t, input))
if err == nil {
t.Fatalf("expected err, got none")
}
if !strings.Contains(err.Error(), "apiserver.config.k8s.io/v99") {
t.Fatalf("expected apiVersion error, got %v", err)
}
}
}
func TestLoadFromReader(t *testing.T) {
// no reader
{
config, err := LoadFromReader(nil)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(config, defaultConfig) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(defaultConfig, config))
}
}
// empty reader
{
config, err := LoadFromReader(&bytes.Buffer{})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(config, defaultConfig) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(defaultConfig, config))
}
}
// valid reader
{
input := `{
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
"kind":"AuthorizationConfiguration",
"authorizers":[{"type":"Webhook"}]}`
expect := &api.AuthorizationConfiguration{
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
}
config, err := LoadFromReader(bytes.NewBufferString(input))
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(config, expect) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(expect, config))
}
}
// invalid reader
{
input := `{
"apiVersion":"apiserver.config.k8s.io/v99",
"kind":"AuthorizationConfiguration",
"authorizers":[{"type":"Webhook"}]}`
_, err := LoadFromReader(bytes.NewBufferString(input))
if err == nil {
t.Fatalf("expected err, got none")
}
if !strings.Contains(err.Error(), "apiserver.config.k8s.io/v99") {
t.Fatalf("expected apiVersion error, got %v", err)
}
}
}
func TestLoadFromData(t *testing.T) {
testcases := []struct {
name string
data []byte
expectErr string
expectConfig *api.AuthorizationConfiguration
}{
{
name: "nil",
data: nil,
expectConfig: defaultConfig,
},
{
name: "nil",
data: []byte{},
expectConfig: defaultConfig,
},
{
name: "v1alpha1 - json",
data: []byte(`{
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
"kind":"AuthorizationConfiguration",
"authorizers":[{"type":"Webhook"}]}`),
expectConfig: &api.AuthorizationConfiguration{
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
},
},
{
name: "v1alpha1 - defaults",
data: []byte(`{
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
"kind":"AuthorizationConfiguration",
"authorizers":[{"type":"Webhook","name":"default","webhook":{}}]}`),
expectConfig: &api.AuthorizationConfiguration{
Authorizers: []api.AuthorizerConfiguration{{
Type: "Webhook",
Name: "default",
Webhook: &api.WebhookConfiguration{
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
},
}},
},
},
{
name: "v1alpha1 - yaml",
data: []byte(`
apiVersion: apiserver.config.k8s.io/v1alpha1
kind: AuthorizationConfiguration
authorizers:
- type: Webhook
`),
expectConfig: &api.AuthorizationConfiguration{
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
},
},
{
name: "missing apiVersion",
data: []byte(`{"kind":"AuthorizationConfiguration"}`),
expectErr: `'apiVersion' is missing`,
},
{
name: "missing kind",
data: []byte(`{"apiVersion":"apiserver.config.k8s.io/v1alpha1"}`),
expectErr: `'Kind' is missing`,
},
{
name: "unknown group",
data: []byte(`{"apiVersion":"apps/v1alpha1","kind":"AuthorizationConfiguration"}`),
expectErr: `apps/v1alpha1`,
},
{
name: "unknown version",
data: []byte(`{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthorizationConfiguration"}`),
expectErr: `apiserver.config.k8s.io/v99`,
},
{
name: "unknown kind",
data: []byte(`{"apiVersion":"apiserver.config.k8s.io/v1alpha1","kind":"SomeConfiguration"}`),
expectErr: `SomeConfiguration`,
},
{
name: "unknown field",
data: []byte(`{
"apiVersion":"apiserver.config.k8s.io/v1alpha1",
"kind":"AuthorizationConfiguration",
"authorzers":[{"type":"Webhook"}]}`),
expectErr: `unknown field "authorzers"`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
config, err := LoadFromData(tc.data)
if err != nil {
if len(tc.expectErr) == 0 {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(err.Error(), tc.expectErr) {
t.Fatalf("unexpected error: %v", err)
}
return
}
if len(tc.expectErr) > 0 {
t.Fatalf("expected err, got none")
}
if !reflect.DeepEqual(config, tc.expectConfig) {
t.Fatalf("unexpected config:\n%s", cmp.Diff(tc.expectConfig, config))
}
})
}
}

1
vendor/modules.txt vendored
View File

@ -1457,6 +1457,7 @@ k8s.io/apiserver/pkg/admission/plugin/webhook/validating
k8s.io/apiserver/pkg/admission/testing
k8s.io/apiserver/pkg/apis/apiserver
k8s.io/apiserver/pkg/apis/apiserver/install
k8s.io/apiserver/pkg/apis/apiserver/load
k8s.io/apiserver/pkg/apis/apiserver/v1
k8s.io/apiserver/pkg/apis/apiserver/v1alpha1
k8s.io/apiserver/pkg/apis/apiserver/v1beta1