KEP-4601: Graduate selector authorization to stable

This commit is contained in:
Jordan Liggitt 2025-07-01 12:11:54 -04:00
parent b99ca3f736
commit a04e7cf5eb
No known key found for this signature in database
17 changed files with 71 additions and 65 deletions

View file

@ -88,15 +88,9 @@ type ResourceAttributes struct {
// Name is the name of the resource being requested for a "get" or deleted for a "delete". "" (empty) means all.
Name string
// fieldSelector describes the limitation on access based on field. It can only limit access, not broaden it.
//
// This field is alpha-level. To use this field, you must enable the
// `AuthorizeWithSelectors` feature gate (disabled by default).
// +optional
FieldSelector *FieldSelectorAttributes
// labelSelector describes the limitation on access based on labels. It can only limit access, not broaden it.
//
// This field is alpha-level. To use this field, you must enable the
// `AuthorizeWithSelectors` feature gate (disabled by default).
// +optional
LabelSelector *LabelSelectorAttributes
}

View file

@ -1029,6 +1029,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
AuthorizeNodeWithSelectors: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37
},
CPUCFSQuotaPeriod: {
@ -1753,6 +1754,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
genericfeatures.AuthorizeWithSelectors: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37
},
genericfeatures.BtreeWatchCache: {

View file

@ -29,10 +29,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
@ -236,8 +233,6 @@ func TestCreate(t *testing.T) {
},
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
for k, tc := range testcases {
auth := &fakeAuthorizer{
decision: tc.decision,

View file

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
@ -636,7 +637,10 @@ func TestAuthorizationAttributesFrom(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tt.enableAuthorizationSelector)
if !tt.enableAuthorizationSelector {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, false)
}
if got := AuthorizationAttributesFrom(tt.args.spec); !reflect.DeepEqual(got, tt.want) {
if got.LabelSelectorParsingErr != nil {

View file

@ -32,6 +32,7 @@ import (
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
@ -72,23 +73,33 @@ func TestNodeAuthorizer(t *testing.T) {
nodeunregistered := &user.DefaultInfo{Name: "system:node:nodeunregistered", Groups: []string{"system:nodes"}}
selectorAuthzDisabled := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzDisabled, genericfeatures.AuthorizeWithSelectors, false)
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzDisabled, features.AuthorizeNodeWithSelectors, false)
selectorAuthzDisabled := func(t testing.TB) featuregate.FeatureGate {
f := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, f, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, f, genericfeatures.AuthorizeWithSelectors, false)
featuregatetesting.SetFeatureGateDuringTest(t, f, features.AuthorizeNodeWithSelectors, false)
return f
}
selectorAuthzEnabled := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, genericfeatures.AuthorizeWithSelectors, true)
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, features.AuthorizeNodeWithSelectors, true)
selectorAuthzEnabled := func(t testing.TB) featuregate.FeatureGate {
return utilfeature.DefaultFeatureGate
}
serviceAccountTokenForCredentialProvidersDisabled := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, serviceAccountTokenForCredentialProvidersDisabled, features.KubeletServiceAccountTokenForCredentialProviders, false)
serviceAccountTokenForCredentialProvidersDisabled := func(t testing.TB) featuregate.FeatureGate {
f := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, f, features.KubeletServiceAccountTokenForCredentialProviders, false)
return f
}
serviceAccountTokenForCredentialProvidersEnabled := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, serviceAccountTokenForCredentialProvidersEnabled, features.KubeletServiceAccountTokenForCredentialProviders, true)
serviceAccountTokenForCredentialProvidersEnabled := func(t testing.TB) featuregate.FeatureGate {
f := utilfeature.DefaultFeatureGate.DeepCopy()
featuregatetesting.SetFeatureGateDuringTest(t, f, features.KubeletServiceAccountTokenForCredentialProviders, true)
return f
}
featureVariants := []struct {
suffix string
features featuregate.FeatureGate
features func(t testing.TB) featuregate.FeatureGate
}{
{suffix: "selector_disabled", features: selectorAuthzDisabled},
{suffix: "selector_enabled", features: selectorAuthzEnabled},
@ -99,7 +110,7 @@ func TestNodeAuthorizer(t *testing.T) {
attrs authorizer.AttributesRecord
expect authorizer.Decision
expectReason string
features featuregate.FeatureGate
features func(t testing.TB) featuregate.FeatureGate
}{
{
name: "allowed configmap",
@ -770,7 +781,7 @@ func TestNodeAuthorizer(t *testing.T) {
if tc.features == nil {
for _, variant := range featureVariants {
t.Run(tc.name+"_"+variant.suffix, func(t *testing.T) {
authz.features = variant.features
authz.features = variant.features(t)
decision, reason, _ := authz.Authorize(context.Background(), tc.attrs)
if decision != tc.expect {
t.Errorf("expected %v, got %v (%s)", tc.expect, decision, reason)
@ -779,7 +790,7 @@ func TestNodeAuthorizer(t *testing.T) {
}
} else {
t.Run(tc.name, func(t *testing.T) {
authz.features = tc.features
authz.features = tc.features(t)
decision, reason, _ := authz.Authorize(context.Background(), tc.attrs)
if decision != tc.expect {
t.Errorf("expected %v, got %v (%s)", tc.expect, decision, reason)

View file

@ -119,15 +119,9 @@ type ResourceAttributes struct {
// +optional
Name string `json:"name,omitempty" protobuf:"bytes,7,opt,name=name"`
// fieldSelector describes the limitation on access based on field. It can only limit access, not broaden it.
//
// This field is alpha-level. To use this field, you must enable the
// `AuthorizeWithSelectors` feature gate (disabled by default).
// +optional
FieldSelector *FieldSelectorAttributes `json:"fieldSelector,omitempty" protobuf:"bytes,8,opt,name=fieldSelector"`
// labelSelector describes the limitation on access based on labels. It can only limit access, not broaden it.
//
// This field is alpha-level. To use this field, you must enable the
// `AuthorizeWithSelectors` feature gate (disabled by default).
// +optional
LabelSelector *LabelSelectorAttributes `json:"labelSelector,omitempty" protobuf:"bytes,9,opt,name=labelSelector"`
}

View file

@ -26,9 +26,6 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
)
func mustParseLabelSelector(str string) labels.Requirements {
@ -41,8 +38,6 @@ func mustParseLabelSelector(str string) labels.Requirements {
}
func TestCachingAuthorizer(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
type result struct {
decision authorizer.Decision
reason string

View file

@ -920,7 +920,10 @@ func TestCondition(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
environment.DisableBaseEnvSetCachingForTests()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tc.enableSelectors)
if !tc.enableSelectors {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, false)
}
if tc.testPerCallLimit == 0 {
tc.testPerCallLimit = celconfig.PerCallLimit

View file

@ -24,6 +24,7 @@ import (
v1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -98,7 +99,10 @@ func TestCompileCELExpression(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tc.authorizeWithSelectorsEnabled)
if !tc.authorizeWithSelectorsEnabled {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, false)
}
// create new compiler because it depends on the feature gate
compiler := NewDefaultCompiler()
@ -117,7 +121,6 @@ func TestCompileCELExpression(t *testing.T) {
}
func TestBuildRequestType(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
f := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}

View file

@ -27,11 +27,9 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"github.com/stretchr/testify/assert"
batch "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
@ -47,10 +45,9 @@ func TestGetAuthorizerAttributes(t *testing.T) {
}
testcases := map[string]struct {
Verb string
Path string
ExpectedAttributes *authorizer.AttributesRecord
EnableAuthorizationSelector bool
Verb string
Path string
ExpectedAttributes *authorizer.AttributesRecord
}{
"non-resource root": {
Verb: http.MethodPost,
@ -143,7 +140,6 @@ func TestGetAuthorizerAttributes(t *testing.T) {
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad field selector": {
Verb: http.MethodGet,
@ -158,7 +154,6 @@ func TestGetAuthorizerAttributes(t *testing.T) {
Resource: "jobs",
FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"),
},
EnableAuthorizationSelector: true,
},
"disabled, ignore good label selector": {
Verb: http.MethodGet,
@ -188,7 +183,6 @@ func TestGetAuthorizerAttributes(t *testing.T) {
*basicLabelRequirement,
},
},
EnableAuthorizationSelector: true,
},
"enabled, bad label selector": {
Verb: http.MethodGet,
@ -203,16 +197,12 @@ func TestGetAuthorizerAttributes(t *testing.T) {
Resource: "jobs",
LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"*bar\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
},
EnableAuthorizationSelector: true,
},
}
for k, tc := range testcases {
t.Run(k, func(t *testing.T) {
ctx := t.Context()
if tc.EnableAuthorizationSelector {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
}
req, _ := http.NewRequestWithContext(ctx, tc.Verb, tc.Path, nil)
req.RemoteAddr = "127.0.0.1"

View file

@ -25,9 +25,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
)
func TestGetAPIRequestInfo(t *testing.T) {
@ -315,8 +312,6 @@ func TestSelectorParsing(t *testing.T) {
resolver := newTestRequestInfoResolver()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
for _, tc := range tests {
ctx := t.Context()
req, _ := http.NewRequestWithContext(ctx, tc.method, tc.url, nil)

View file

@ -301,6 +301,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
AuthorizeWithSelectors: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37
},
BtreeWatchCache: {

View file

@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -322,7 +323,10 @@ func Test_resourceAttributesFrom(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tt.enableAuthorizationSelector)
if !tt.enableAuthorizationSelector {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, false)
}
if got := resourceAttributesFrom(tt.args.attr); !reflect.DeepEqual(got, tt.want) {
t.Errorf("resourceAttributesFrom() = %v, want %v", got, tt.want)

View file

@ -41,6 +41,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user"
@ -783,7 +784,10 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, test.selectorEnabled)
if !test.selectorEnabled {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, false)
}
// create new compiler because it depends on the feature gate
compiler := authorizationcel.NewDefaultCompiler()

View file

@ -141,6 +141,10 @@
lockToDefault: false
preRelease: Beta
version: "1.32"
- default: true
lockToDefault: true
preRelease: GA
version: "1.34"
- name: AuthorizeWithSelectors
versionedSpecs:
- default: false
@ -151,6 +155,10 @@
lockToDefault: false
preRelease: Beta
version: "1.32"
- default: true
lockToDefault: true
preRelease: GA
version: "1.34"
- name: BtreeWatchCache
versionedSpecs:
- default: true

View file

@ -30,8 +30,11 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/environment"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
"k8s.io/utils/ptr"
@ -46,6 +49,10 @@ func RunAuthzSelectorsLibraryTests(t *testing.T, featureEnabled bool) {
t.Fatalf("authz selector library was initialized before feature gates were finalized (possibly from an init() or package variable)")
}
if !featureEnabled {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
}
// Start the server with the desired feature enablement
server, err := apiservertesting.StartTestServer(t, nil, []string{
fmt.Sprintf("--feature-gates=AuthorizeNodeWithSelectors=%v,AuthorizeWithSelectors=%v", featureEnabled, featureEnabled),

View file

@ -41,13 +41,10 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
"k8s.io/apiserver/pkg/features"
authzmetrics "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
utilfeature "k8s.io/apiserver/pkg/util/feature"
webhookmetrics "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
featuregatetesting "k8s.io/component-base/featuregate/testing"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/authutil"
"k8s.io/kubernetes/test/integration/framework"
@ -124,7 +121,6 @@ authorizers:
func TestMultiWebhookAuthzConfig(t *testing.T) {
authzmetrics.ResetMetricsForTest()
defer authzmetrics.ResetMetricsForTest()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, true)
dir := t.TempDir()