Skip to content

Commit ca43bf0

Browse files
authored
Merge pull request kubernetes#120154 from palnabarun/authz-config-external-changes
[StructuredAuthorizationConfiguration] Add --authorization-config flag and guard it using a Feature Gate
2 parents dc8b57d + a50d83c commit ca43bf0

File tree

10 files changed

+638
-11
lines changed

10 files changed

+638
-11
lines changed

cmd/kube-apiserver/app/options/options_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ func TestAddFlags(t *testing.T) {
327327
expected.Authentication.OIDC.UsernameClaim = "sub"
328328
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
329329

330+
if !s.Authorization.AreLegacyFlagsSet() {
331+
t.Errorf("expected legacy authorization flags to be set")
332+
}
333+
334+
// setting the method to nil since methods can't be compared with reflect.DeepEqual
335+
s.Authorization.AreLegacyFlagsSet = nil
336+
330337
if !reflect.DeepEqual(expected, s) {
331338
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{})))
332339
}

pkg/controlplane/apiserver/options/options_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@ func TestAddFlags(t *testing.T) {
283283
expected.Authentication.OIDC.UsernameClaim = "sub"
284284
expected.Authentication.OIDC.SigningAlgs = []string{"RS256"}
285285

286+
if !s.Authorization.AreLegacyFlagsSet() {
287+
t.Errorf("expected legacy authorization flags to be set")
288+
}
289+
// setting the method to nil since methods can't be compared with reflect.DeepEqual
290+
s.Authorization.AreLegacyFlagsSet = nil
291+
286292
if !reflect.DeepEqual(expected, s) {
287293
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{})))
288294
}

pkg/features/kube_features.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
11361136

11371137
genericfeatures.ServerSideFieldValidation: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29
11381138

1139+
genericfeatures.StructuredAuthorizationConfiguration: {Default: false, PreRelease: featuregate.Alpha},
1140+
11391141
genericfeatures.UnauthenticatedHTTP2DOSMitigation: {Default: true, PreRelease: featuregate.Beta},
11401142

11411143
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed

pkg/kubeapiserver/options/authorization.go

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ import (
2121
"strings"
2222
"time"
2323

24+
"k8s.io/apiserver/pkg/apis/apiserver/load"
25+
genericfeatures "k8s.io/apiserver/pkg/features"
26+
utilfeature "k8s.io/apiserver/pkg/util/feature"
27+
2428
"github.com/spf13/pflag"
2529

2630
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2731
"k8s.io/apimachinery/pkg/util/sets"
2832
"k8s.io/apimachinery/pkg/util/wait"
2933
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
34+
"k8s.io/apiserver/pkg/apis/apiserver/validation"
3035
genericoptions "k8s.io/apiserver/pkg/server/options"
3136
versionedinformers "k8s.io/client-go/informers"
3237

@@ -35,9 +40,19 @@ import (
3540
)
3641

3742
const (
38-
defaultWebhookName = "default"
43+
defaultWebhookName = "default"
44+
authorizationModeFlag = "authorization-mode"
45+
authorizationWebhookConfigFileFlag = "authorization-webhook-config-file"
46+
authorizationWebhookVersionFlag = "authorization-webhook-version"
47+
authorizationWebhookAuthorizedTTLFlag = "authorization-webhook-cache-authorized-ttl"
48+
authorizationWebhookUnauthorizedTTLFlag = "authorization-webhook-cache-unauthorized-ttl"
49+
authorizationPolicyFileFlag = "authorization-policy-file"
50+
authorizationConfigFlag = "authorization-config"
3951
)
4052

53+
// RepeatableAuthorizerTypes is the list of Authorizer that can be repeated in the Authorization Config
54+
var repeatableAuthorizerTypes = []string{authzmodes.ModeWebhook}
55+
4156
// BuiltInAuthorizationOptions contains all build-in authorization options for API Server
4257
type BuiltInAuthorizationOptions struct {
4358
Modes []string
@@ -50,6 +65,16 @@ type BuiltInAuthorizationOptions struct {
5065
// This allows us to configure the sleep time at each iteration and the maximum number of retries allowed
5166
// before we fail the webhook call in order to limit the fan out that ensues when the system is degraded.
5267
WebhookRetryBackoff *wait.Backoff
68+
69+
// AuthorizationConfigurationFile is mutually exclusive with all of:
70+
// - Modes
71+
// - WebhookConfigFile
72+
// - WebHookVersion
73+
// - WebhookCacheAuthorizedTTL
74+
// - WebhookCacheUnauthorizedTTL
75+
AuthorizationConfigurationFile string
76+
77+
AreLegacyFlagsSet func() bool
5378
}
5479

5580
// NewBuiltInAuthorizationOptions create a BuiltInAuthorizationOptions with default value
@@ -69,6 +94,54 @@ func (o *BuiltInAuthorizationOptions) Validate() []error {
6994
return nil
7095
}
7196
var allErrors []error
97+
98+
// if --authorization-config is set, check if
99+
// - the feature flag is set
100+
// - legacyFlags are not set
101+
// - the config file can be loaded
102+
// - the config file represents a valid configuration
103+
if o.AuthorizationConfigurationFile != "" {
104+
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
105+
return append(allErrors, fmt.Errorf("--%s cannot be used without enabling StructuredAuthorizationConfiguration feature flag", authorizationConfigFlag))
106+
}
107+
108+
// error out if legacy flags are defined
109+
if o.AreLegacyFlagsSet != nil && o.AreLegacyFlagsSet() {
110+
return append(allErrors, fmt.Errorf("--%s can not be specified when --%s or --authorization-webhook-* flags are defined", authorizationConfigFlag, authorizationModeFlag))
111+
}
112+
113+
// load the file and check for errors
114+
config, err := load.LoadFromFile(o.AuthorizationConfigurationFile)
115+
if err != nil {
116+
return append(allErrors, fmt.Errorf("failed to load AuthorizationConfiguration from file: %v", err))
117+
}
118+
119+
// validate the file and return any error
120+
if errors := validation.ValidateAuthorizationConfiguration(nil, config,
121+
sets.NewString(authzmodes.AuthorizationModeChoices...),
122+
sets.NewString(repeatableAuthorizerTypes...),
123+
); len(errors) != 0 {
124+
allErrors = append(allErrors, errors.ToAggregate().Errors()...)
125+
}
126+
127+
// test to check if the authorizer names passed conform to the authorizers for type!=Webhook
128+
// this test is only for kube-apiserver and hence checked here
129+
// it preserves compatibility with o.buildAuthorizationConfiguration
130+
for _, authorizer := range config.Authorizers {
131+
if string(authorizer.Type) == authzmodes.ModeWebhook {
132+
continue
133+
}
134+
135+
expectedName := getNameForAuthorizerMode(string(authorizer.Type))
136+
if expectedName != authorizer.Name {
137+
allErrors = append(allErrors, fmt.Errorf("expected name %s for authorizer %s instead of %s", expectedName, authorizer.Type, authorizer.Name))
138+
}
139+
}
140+
141+
return allErrors
142+
}
143+
144+
// validate the legacy flags using the legacy mode if --authorization-config is not passed
72145
if len(o.Modes) == 0 {
73146
allErrors = append(allErrors, fmt.Errorf("at least one authorization-mode must be passed"))
74147
}
@@ -111,27 +184,47 @@ func (o *BuiltInAuthorizationOptions) AddFlags(fs *pflag.FlagSet) {
111184
return
112185
}
113186

114-
fs.StringSliceVar(&o.Modes, "authorization-mode", o.Modes, ""+
187+
fs.StringSliceVar(&o.Modes, authorizationModeFlag, o.Modes, ""+
115188
"Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+
116189
strings.Join(authzmodes.AuthorizationModeChoices, ",")+".")
117190

118-
fs.StringVar(&o.PolicyFile, "authorization-policy-file", o.PolicyFile, ""+
191+
fs.StringVar(&o.PolicyFile, authorizationPolicyFileFlag, o.PolicyFile, ""+
119192
"File with authorization policy in json line by line format, used with --authorization-mode=ABAC, on the secure port.")
120193

121-
fs.StringVar(&o.WebhookConfigFile, "authorization-webhook-config-file", o.WebhookConfigFile, ""+
194+
fs.StringVar(&o.WebhookConfigFile, authorizationWebhookConfigFileFlag, o.WebhookConfigFile, ""+
122195
"File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. "+
123196
"The API server will query the remote service to determine access on the API server's secure port.")
124197

125-
fs.StringVar(&o.WebhookVersion, "authorization-webhook-version", o.WebhookVersion, ""+
198+
fs.StringVar(&o.WebhookVersion, authorizationWebhookVersionFlag, o.WebhookVersion, ""+
126199
"The API version of the authorization.k8s.io SubjectAccessReview to send to and expect from the webhook.")
127200

128-
fs.DurationVar(&o.WebhookCacheAuthorizedTTL, "authorization-webhook-cache-authorized-ttl",
201+
fs.DurationVar(&o.WebhookCacheAuthorizedTTL, authorizationWebhookAuthorizedTTLFlag,
129202
o.WebhookCacheAuthorizedTTL,
130203
"The duration to cache 'authorized' responses from the webhook authorizer.")
131204

132205
fs.DurationVar(&o.WebhookCacheUnauthorizedTTL,
133-
"authorization-webhook-cache-unauthorized-ttl", o.WebhookCacheUnauthorizedTTL,
206+
authorizationWebhookUnauthorizedTTLFlag, o.WebhookCacheUnauthorizedTTL,
134207
"The duration to cache 'unauthorized' responses from the webhook authorizer.")
208+
209+
fs.StringVar(&o.AuthorizationConfigurationFile, authorizationConfigFlag, o.AuthorizationConfigurationFile, ""+
210+
"File with Authorization Configuration to configure the authorizer chain."+
211+
"Note: This feature is in Alpha since v1.29."+
212+
"--feature-gate=StructuredAuthorizationConfiguration=true feature flag needs to be set to true for enabling the functionality."+
213+
"This feature is mutually exclusive with the other --authorization-mode and --authorization-webhook-* flags.")
214+
215+
// preserves compatibility with any method set during initialization
216+
oldAreLegacyFlagsSet := o.AreLegacyFlagsSet
217+
o.AreLegacyFlagsSet = func() bool {
218+
if oldAreLegacyFlagsSet != nil && oldAreLegacyFlagsSet() {
219+
return true
220+
}
221+
222+
return fs.Changed(authorizationModeFlag) ||
223+
fs.Changed(authorizationWebhookConfigFileFlag) ||
224+
fs.Changed(authorizationWebhookVersionFlag) ||
225+
fs.Changed(authorizationWebhookAuthorizedTTLFlag) ||
226+
fs.Changed(authorizationWebhookUnauthorizedTTLFlag)
227+
}
135228
}
136229

137230
// ToAuthorizationConfig convert BuiltInAuthorizationOptions to authorizer.Config
@@ -140,17 +233,52 @@ func (o *BuiltInAuthorizationOptions) ToAuthorizationConfig(versionedInformerFac
140233
return nil, nil
141234
}
142235

143-
authzConfiguration, err := o.buildAuthorizationConfiguration()
144-
if err != nil {
145-
return nil, fmt.Errorf("failed to build authorization config: %s", err)
236+
var authorizationConfiguration *authzconfig.AuthorizationConfiguration
237+
var err error
238+
239+
// if --authorization-config is set, check if
240+
// - the feature flag is set
241+
// - legacyFlags are not set
242+
// - the config file can be loaded
243+
// - the config file represents a valid configuration
244+
// else,
245+
// - build the AuthorizationConfig from the legacy flags
246+
if o.AuthorizationConfigurationFile != "" {
247+
if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
248+
return nil, fmt.Errorf("--%s cannot be used without enabling StructuredAuthorizationConfiguration feature flag", authorizationConfigFlag)
249+
}
250+
251+
// error out if legacy flags are defined
252+
if o.AreLegacyFlagsSet != nil && o.AreLegacyFlagsSet() {
253+
return nil, fmt.Errorf("--%s can not be specified when --%s or --authorization-webhook-* flags are defined", authorizationConfigFlag, authorizationModeFlag)
254+
}
255+
256+
// load the file and check for errors
257+
authorizationConfiguration, err = load.LoadFromFile(o.AuthorizationConfigurationFile)
258+
if err != nil {
259+
return nil, fmt.Errorf("failed to load AuthorizationConfiguration from file: %v", err)
260+
}
261+
262+
// validate the file and return any error
263+
if errors := validation.ValidateAuthorizationConfiguration(nil, authorizationConfiguration,
264+
sets.NewString(authzmodes.AuthorizationModeChoices...),
265+
sets.NewString(repeatableAuthorizerTypes...),
266+
); len(errors) != 0 {
267+
return nil, fmt.Errorf(errors.ToAggregate().Error())
268+
}
269+
} else {
270+
authorizationConfiguration, err = o.buildAuthorizationConfiguration()
271+
if err != nil {
272+
return nil, fmt.Errorf("failed to build authorization config: %s", err)
273+
}
146274
}
147275

148276
return &authorizer.Config{
149277
PolicyFile: o.PolicyFile,
150278
VersionedInformerFactory: versionedInformerFactory,
151279
WebhookRetryBackoff: o.WebhookRetryBackoff,
152280

153-
AuthorizationConfiguration: authzConfiguration,
281+
AuthorizationConfiguration: authorizationConfiguration,
154282
}, nil
155283
}
156284

pkg/kubeapiserver/options/authorization_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ func TestBuiltInAuthorizationOptionsAddFlags(t *testing.T) {
173173
t.Fatal(err)
174174
}
175175

176+
if !opts.AreLegacyFlagsSet() {
177+
t.Fatal("legacy flags should have been configured")
178+
}
179+
180+
// setting the method to nil since methods can't be compared with reflect.DeepEqual
181+
opts.AreLegacyFlagsSet = nil
182+
176183
if !reflect.DeepEqual(opts, expected) {
177184
t.Error(cmp.Diff(opts, expected))
178185
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package load
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"os"
23+
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/serializer"
26+
api "k8s.io/apiserver/pkg/apis/apiserver"
27+
"k8s.io/apiserver/pkg/apis/apiserver/install"
28+
externalapi "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
29+
)
30+
31+
var (
32+
scheme = runtime.NewScheme()
33+
codecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict)
34+
)
35+
36+
func init() {
37+
install.Install(scheme)
38+
}
39+
40+
func LoadFromFile(file string) (*api.AuthorizationConfiguration, error) {
41+
data, err := os.ReadFile(file)
42+
if err != nil {
43+
return nil, err
44+
}
45+
return LoadFromData(data)
46+
}
47+
48+
func LoadFromReader(reader io.Reader) (*api.AuthorizationConfiguration, error) {
49+
if reader == nil {
50+
// no reader specified, use default config
51+
return LoadFromData(nil)
52+
}
53+
54+
data, err := io.ReadAll(reader)
55+
if err != nil {
56+
return nil, err
57+
}
58+
return LoadFromData(data)
59+
}
60+
61+
func LoadFromData(data []byte) (*api.AuthorizationConfiguration, error) {
62+
if len(data) == 0 {
63+
// no config provided, return default
64+
externalConfig := &externalapi.AuthorizationConfiguration{}
65+
scheme.Default(externalConfig)
66+
internalConfig := &api.AuthorizationConfiguration{}
67+
if err := scheme.Convert(externalConfig, internalConfig, nil); err != nil {
68+
return nil, err
69+
}
70+
return internalConfig, nil
71+
}
72+
73+
decodedObj, err := runtime.Decode(codecs.UniversalDecoder(), data)
74+
if err != nil {
75+
return nil, err
76+
}
77+
configuration, ok := decodedObj.(*api.AuthorizationConfiguration)
78+
if !ok {
79+
return nil, fmt.Errorf("expected AuthorizationConfiguration, got %T", decodedObj)
80+
}
81+
return configuration, nil
82+
}

0 commit comments

Comments
 (0)