add field and label selectors to authorization attributes
Co-authored-by: Jordan Liggitt <liggitt@google.com> Kubernetes-commit: 92e3445e9d7a587ddb56b3ff4b1445244fbf9abd
This commit is contained in:
parent
6dd5496a01
commit
f26d4ed894
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
@ -58,6 +60,8 @@ var _ authorizer.Attributes = (interface {
|
||||||
GetAPIVersion() string
|
GetAPIVersion() string
|
||||||
IsResourceRequest() bool
|
IsResourceRequest() bool
|
||||||
GetPath() string
|
GetPath() string
|
||||||
|
GetFieldSelector() (fields.Requirements, error)
|
||||||
|
GetLabelSelector() (labels.Requirements, error)
|
||||||
})(nil)
|
})(nil)
|
||||||
|
|
||||||
// The user info accessors known to cache key construction. If this fails to compile, the cache
|
// The user info accessors known to cache key construction. If this fails to compile, the cache
|
||||||
|
|
@ -72,16 +76,31 @@ var _ user.Info = (interface {
|
||||||
// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent
|
// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent
|
||||||
// check has already been performed, a cached result is returned. Not safe for concurrent use.
|
// check has already been performed, a cached result is returned. Not safe for concurrent use.
|
||||||
func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
serializableAttributes := authorizer.AttributesRecord{
|
type SerializableAttributes struct {
|
||||||
Verb: a.GetVerb(),
|
authorizer.AttributesRecord
|
||||||
Namespace: a.GetNamespace(),
|
LabelSelector string
|
||||||
APIGroup: a.GetAPIGroup(),
|
}
|
||||||
APIVersion: a.GetAPIVersion(),
|
|
||||||
Resource: a.GetResource(),
|
serializableAttributes := SerializableAttributes{
|
||||||
Subresource: a.GetSubresource(),
|
AttributesRecord: authorizer.AttributesRecord{
|
||||||
Name: a.GetName(),
|
Verb: a.GetVerb(),
|
||||||
ResourceRequest: a.IsResourceRequest(),
|
Namespace: a.GetNamespace(),
|
||||||
Path: a.GetPath(),
|
APIGroup: a.GetAPIGroup(),
|
||||||
|
APIVersion: a.GetAPIVersion(),
|
||||||
|
Resource: a.GetResource(),
|
||||||
|
Subresource: a.GetSubresource(),
|
||||||
|
Name: a.GetName(),
|
||||||
|
ResourceRequest: a.IsResourceRequest(),
|
||||||
|
Path: a.GetPath(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// in the error case, we won't honor this field selector, so the cache doesn't need it.
|
||||||
|
if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 {
|
||||||
|
serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err
|
||||||
|
}
|
||||||
|
if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 {
|
||||||
|
// the labels requirements have private elements so those don't help us serialize to a unique key
|
||||||
|
serializableAttributes.LabelSelector = labelSelector.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if u := a.GetUser(); u != nil {
|
if u := a.GetUser(); u != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,31 @@ package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"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 {
|
||||||
|
ret, err := labels.Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
retRequirements, _ /*selectable*/ := ret.Requirements()
|
||||||
|
return retRequirements
|
||||||
|
}
|
||||||
|
|
||||||
func TestCachingAuthorizer(t *testing.T) {
|
func TestCachingAuthorizer(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||||
|
|
||||||
type result struct {
|
type result struct {
|
||||||
decision authorizer.Decision
|
decision authorizer.Decision
|
||||||
reason string
|
reason string
|
||||||
|
|
@ -216,6 +233,261 @@ func TestCachingAuthorizer(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "honor good field selector",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 2",
|
||||||
|
error: fmt.Errorf("test error 2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// now this should be cached
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 2",
|
||||||
|
error: fmt.Errorf("test error 2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore malformed field selector first",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
FieldSelectorParsingErr: errors.New("malformed"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// notice that this does not have the malformed field selector.
|
||||||
|
// it should use the cached result
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore malformed field selector second",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this should use the broader cached value because the selector will be ignored
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
FieldSelectorParsingErr: errors.New("malformed"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "honor good label selector",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 2",
|
||||||
|
error: fmt.Errorf("test error 2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// now this should be cached
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
LabelSelectorRequirements: mustParseLabelSelector("diff=zero"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 3",
|
||||||
|
error: fmt.Errorf("test error 3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 2",
|
||||||
|
error: fmt.Errorf("test error 2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason 3",
|
||||||
|
error: fmt.Errorf("test error 3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore malformed label selector first",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
LabelSelectorParsingErr: errors.New("malformed mess"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// notice that this does not have the malformed field selector.
|
||||||
|
// it should use the cached result
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore malformed label selector second",
|
||||||
|
calls: []invocation{
|
||||||
|
{
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this should use the broader cached value because the selector will be ignored
|
||||||
|
attributes: authorizer.AttributesRecord{
|
||||||
|
Name: "test name",
|
||||||
|
LabelSelectorParsingErr: errors.New("malformed mess"),
|
||||||
|
},
|
||||||
|
expected: result{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: []result{
|
||||||
|
{
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
reason: "test reason",
|
||||||
|
error: fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var misses int
|
var misses int
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -62,6 +64,16 @@ type Attributes interface {
|
||||||
|
|
||||||
// GetPath returns the path of the request
|
// GetPath returns the path of the request
|
||||||
GetPath() string
|
GetPath() string
|
||||||
|
|
||||||
|
// ParseFieldSelector is lazy, thread-safe, and stores the parsed result and error.
|
||||||
|
// It returns an error if the field selector cannot be parsed.
|
||||||
|
// The returned requirements must be treated as readonly and not modified.
|
||||||
|
GetFieldSelector() (fields.Requirements, error)
|
||||||
|
|
||||||
|
// ParseLabelSelector is lazy, thread-safe, and stores the parsed result and error.
|
||||||
|
// It returns an error if the label selector cannot be parsed.
|
||||||
|
// The returned requirements must be treated as readonly and not modified.
|
||||||
|
GetLabelSelector() (labels.Requirements, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorizer makes an authorization decision based on information gained by making
|
// Authorizer makes an authorization decision based on information gained by making
|
||||||
|
|
@ -100,6 +112,11 @@ type AttributesRecord struct {
|
||||||
Name string
|
Name string
|
||||||
ResourceRequest bool
|
ResourceRequest bool
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
FieldSelectorRequirements fields.Requirements
|
||||||
|
FieldSelectorParsingErr error
|
||||||
|
LabelSelectorRequirements labels.Requirements
|
||||||
|
LabelSelectorParsingErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AttributesRecord) GetUser() user.Info {
|
func (a AttributesRecord) GetUser() user.Info {
|
||||||
|
|
@ -146,6 +163,14 @@ func (a AttributesRecord) GetPath() string {
|
||||||
return a.Path
|
return a.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AttributesRecord) GetFieldSelector() (fields.Requirements, error) {
|
||||||
|
return a.FieldSelectorRequirements, a.FieldSelectorParsingErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AttributesRecord) GetLabelSelector() (labels.Requirements, error) {
|
||||||
|
return a.LabelSelectorRequirements, a.LabelSelectorParsingErr
|
||||||
|
}
|
||||||
|
|
||||||
type Decision int
|
type Decision int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -117,5 +122,31 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error)
|
||||||
attribs.Namespace = requestInfo.Namespace
|
attribs.Namespace = requestInfo.Namespace
|
||||||
attribs.Name = requestInfo.Name
|
attribs.Name = requestInfo.Name
|
||||||
|
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
|
||||||
|
// parsing here makes it easy to keep the AttributesRecord type value-only and avoids any mutex copies when
|
||||||
|
// doing shallow copies in other steps.
|
||||||
|
if len(requestInfo.FieldSelector) > 0 {
|
||||||
|
fieldSelector, err := fields.ParseSelector(requestInfo.FieldSelector)
|
||||||
|
if err != nil {
|
||||||
|
attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = nil, err
|
||||||
|
} else {
|
||||||
|
if requirements := fieldSelector.Requirements(); len(requirements) > 0 {
|
||||||
|
attribs.FieldSelectorRequirements, attribs.FieldSelectorParsingErr = fieldSelector.Requirements(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(requestInfo.LabelSelector) > 0 {
|
||||||
|
labelSelector, err := labels.Parse(requestInfo.LabelSelector)
|
||||||
|
if err != nil {
|
||||||
|
attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = nil, err
|
||||||
|
} else {
|
||||||
|
if requirements, _ /*selectable*/ := labelSelector.Requirements(); len(requirements) > 0 {
|
||||||
|
attribs.LabelSelectorRequirements, attribs.LabelSelectorParsingErr = requirements, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &attribs, nil
|
return &attribs, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ package filters
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"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"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -34,10 +40,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetAuthorizerAttributes(t *testing.T) {
|
func TestGetAuthorizerAttributes(t *testing.T) {
|
||||||
|
basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
Verb string
|
Verb string
|
||||||
Path string
|
Path string
|
||||||
ExpectedAttributes *authorizer.AttributesRecord
|
ExpectedAttributes *authorizer.AttributesRecord
|
||||||
|
EnableAuthorizationSelector bool
|
||||||
}{
|
}{
|
||||||
"non-resource root": {
|
"non-resource root": {
|
||||||
Verb: "POST",
|
Verb: "POST",
|
||||||
|
|
@ -102,26 +114,122 @@ func TestGetAuthorizerAttributes(t *testing.T) {
|
||||||
Resource: "jobs",
|
Resource: "jobs",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"disabled, ignore good field selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector%=foo%3Dbar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
Resource: "jobs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled, good field selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=foo%3D%3Dbar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
Resource: "jobs",
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EnableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
"enabled, bad field selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=%2Abar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
Resource: "jobs",
|
||||||
|
FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"),
|
||||||
|
},
|
||||||
|
EnableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
"disabled, ignore good label selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector%=foo%3Dbar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
Resource: "jobs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled, good label selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=foo%3D%3Dbar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
Resource: "jobs",
|
||||||
|
LabelSelectorRequirements: labels.Requirements{
|
||||||
|
*basicLabelRequirement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EnableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
"enabled, bad label selector": {
|
||||||
|
Verb: "GET",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=%2Abar",
|
||||||
|
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||||
|
Verb: "list",
|
||||||
|
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: batch.GroupName,
|
||||||
|
APIVersion: "v1",
|
||||||
|
Namespace: "myns",
|
||||||
|
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 {
|
for k, tc := range testcases {
|
||||||
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
|
t.Run(k, func(t *testing.T) {
|
||||||
req.RemoteAddr = "127.0.0.1"
|
if tc.EnableAuthorizationSelector {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||||
|
}
|
||||||
|
|
||||||
var attribs authorizer.Attributes
|
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
|
||||||
var err error
|
req.RemoteAddr = "127.0.0.1"
|
||||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
ctx := req.Context()
|
var attribs authorizer.Attributes
|
||||||
attribs, err = GetAuthorizerAttributes(ctx)
|
var err error
|
||||||
|
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx := req.Context()
|
||||||
|
attribs, err = GetAuthorizerAttributes(ctx)
|
||||||
|
})
|
||||||
|
handler = WithRequestInfo(handler, newTestRequestInfoResolver())
|
||||||
|
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: unexpected error: %v", k, err)
|
||||||
|
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
|
||||||
|
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
handler = WithRequestInfo(handler, newTestRequestInfoResolver())
|
|
||||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: unexpected error: %v", k, err)
|
|
||||||
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
|
|
||||||
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import (
|
||||||
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
|
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
@ -62,6 +64,13 @@ type RequestInfo struct {
|
||||||
Name string
|
Name string
|
||||||
// Parts are the path parts for the request, always starting with /{resource}/{name}
|
// Parts are the path parts for the request, always starting with /{resource}/{name}
|
||||||
Parts []string
|
Parts []string
|
||||||
|
|
||||||
|
// FieldSelector contains the unparsed field selector from a request. It is only present if the apiserver
|
||||||
|
// honors field selectors for the verb this request is associated with.
|
||||||
|
FieldSelector string
|
||||||
|
// LabelSelector contains the unparsed field selector from a request. It is only present if the apiserver
|
||||||
|
// honors field selectors for the verb this request is associated with.
|
||||||
|
LabelSelector string
|
||||||
}
|
}
|
||||||
|
|
||||||
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
|
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
|
||||||
|
|
@ -77,6 +86,9 @@ var specialVerbsNoSubresources = sets.NewString("proxy")
|
||||||
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
|
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
|
||||||
var namespaceSubresources = sets.NewString("status", "finalize")
|
var namespaceSubresources = sets.NewString("status", "finalize")
|
||||||
|
|
||||||
|
// verbsWithSelectors is the list of verbs which support fieldSelector and labelSelector parameters
|
||||||
|
var verbsWithSelectors = sets.NewString("list", "watch", "deletecollection")
|
||||||
|
|
||||||
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift
|
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/controlplane/master_test.go, so we never drift
|
||||||
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
|
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
|
||||||
|
|
||||||
|
|
@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||||
currentParts = currentParts[1:]
|
currentParts = currentParts[1:]
|
||||||
|
|
||||||
// handle input of form /{specialVerb}/*
|
// handle input of form /{specialVerb}/*
|
||||||
|
verbViaPathPrefix := false
|
||||||
if specialVerbs.Has(currentParts[0]) {
|
if specialVerbs.Has(currentParts[0]) {
|
||||||
if len(currentParts) < 2 {
|
if len(currentParts) < 2 {
|
||||||
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
|
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
|
||||||
|
|
@ -158,6 +171,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||||
|
|
||||||
requestInfo.Verb = currentParts[0]
|
requestInfo.Verb = currentParts[0]
|
||||||
currentParts = currentParts[1:]
|
currentParts = currentParts[1:]
|
||||||
|
verbViaPathPrefix = true
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
|
|
@ -238,11 +252,28 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
|
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
|
||||||
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
|
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
|
||||||
requestInfo.Verb = "deletecollection"
|
requestInfo.Verb = "deletecollection"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
|
||||||
|
// Don't support selector authorization on requests that used the deprecated verb-via-path mechanism, since they don't support selectors consistently.
|
||||||
|
// There are multi-object and single-object watch endpoints, and only the multi-object one supports selectors.
|
||||||
|
if !verbViaPathPrefix && verbsWithSelectors.Has(requestInfo.Verb) {
|
||||||
|
// interestingly these are parsed above, but the current structure there means that if one (or anything) in the
|
||||||
|
// listOptions fails to decode, the field and label selectors are lost.
|
||||||
|
// therefore, do the straight query param read here.
|
||||||
|
if vals := req.URL.Query()["fieldSelector"]; len(vals) > 0 {
|
||||||
|
requestInfo.FieldSelector = vals[0]
|
||||||
|
}
|
||||||
|
if vals := req.URL.Query()["labelSelector"]; len(vals) > 0 {
|
||||||
|
requestInfo.LabelSelector = vals[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &requestInfo, nil
|
return &requestInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ import (
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"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) {
|
func TestGetAPIRequestInfo(t *testing.T) {
|
||||||
|
|
@ -190,64 +193,129 @@ func newTestRequestInfoResolver() *RequestInfoFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFieldSelectorParsing(t *testing.T) {
|
func TestSelectorParsing(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
method string
|
||||||
expectedName string
|
url string
|
||||||
expectedErr error
|
expectedName string
|
||||||
expectedVerb string
|
expectedErr error
|
||||||
|
expectedVerb string
|
||||||
|
expectedFieldSelector string
|
||||||
|
expectedLabelSelector string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no selector",
|
name: "no selector",
|
||||||
url: "/apis/group/version/resource",
|
method: "GET",
|
||||||
expectedVerb: "list",
|
url: "/apis/group/version/resource",
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "metadata.name selector",
|
name: "metadata.name selector",
|
||||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1",
|
method: "GET",
|
||||||
expectedName: "name1",
|
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1",
|
||||||
expectedVerb: "list",
|
expectedName: "name1",
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "metadata.name=name1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "metadata.name selector with watch",
|
name: "metadata.name selector with watch",
|
||||||
url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
|
method: "GET",
|
||||||
expectedName: "name1",
|
url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
|
||||||
expectedVerb: "watch",
|
expectedName: "name1",
|
||||||
|
expectedVerb: "watch",
|
||||||
|
expectedFieldSelector: "metadata.name=name1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "random selector",
|
name: "random selector",
|
||||||
url: "/apis/group/version/resource?fieldSelector=foo=bar",
|
method: "GET",
|
||||||
expectedName: "",
|
url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
|
||||||
expectedVerb: "list",
|
expectedName: "",
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "foo=bar",
|
||||||
|
expectedLabelSelector: "baz=qux",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid selector with metadata.name",
|
name: "invalid selector with metadata.name",
|
||||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
|
method: "GET",
|
||||||
expectedName: "",
|
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
|
||||||
expectedErr: fmt.Errorf("invalid selector"),
|
expectedName: "",
|
||||||
expectedVerb: "list",
|
expectedErr: fmt.Errorf("invalid selector"),
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "metadata.name=name1,foo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid selector with metadata.name with watch",
|
name: "invalid selector with metadata.name with watch",
|
||||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
|
method: "GET",
|
||||||
expectedName: "",
|
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
|
||||||
expectedErr: fmt.Errorf("invalid selector"),
|
expectedName: "",
|
||||||
expectedVerb: "watch",
|
expectedErr: fmt.Errorf("invalid selector"),
|
||||||
|
expectedVerb: "watch",
|
||||||
|
expectedFieldSelector: "metadata.name=name1,foo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid selector with metadata.name with watch false",
|
name: "invalid selector with metadata.name with watch false",
|
||||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
|
method: "GET",
|
||||||
expectedName: "",
|
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
|
||||||
expectedErr: fmt.Errorf("invalid selector"),
|
expectedName: "",
|
||||||
expectedVerb: "list",
|
expectedErr: fmt.Errorf("invalid selector"),
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "metadata.name=name1,foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selector on deletecollection is honored",
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
|
||||||
|
expectedName: "",
|
||||||
|
expectedVerb: "deletecollection",
|
||||||
|
expectedFieldSelector: "foo=bar",
|
||||||
|
expectedLabelSelector: "baz=qux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selector on repeated param matches parsed param",
|
||||||
|
method: "GET",
|
||||||
|
url: "/apis/group/version/resource?fieldSelector=metadata.name=foo&fieldSelector=metadata.name=bar&labelSelector=foo=bar&labelSelector=foo=baz",
|
||||||
|
expectedName: "foo",
|
||||||
|
expectedVerb: "list",
|
||||||
|
expectedFieldSelector: "metadata.name=foo",
|
||||||
|
expectedLabelSelector: "foo=bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selector on other verb is ignored",
|
||||||
|
method: "GET",
|
||||||
|
url: "/apis/group/version/resource/name?fieldSelector=foo=bar&labelSelector=foo=bar",
|
||||||
|
expectedName: "name",
|
||||||
|
expectedVerb: "get",
|
||||||
|
expectedFieldSelector: "",
|
||||||
|
expectedLabelSelector: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selector on deprecated root type watch is not parsed",
|
||||||
|
method: "GET",
|
||||||
|
url: "/apis/group/version/watch/resource?fieldSelector=metadata.name=foo&labelSelector=foo=bar",
|
||||||
|
expectedName: "",
|
||||||
|
expectedVerb: "watch",
|
||||||
|
expectedFieldSelector: "",
|
||||||
|
expectedLabelSelector: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selector on deprecated root item watch is not parsed",
|
||||||
|
method: "GET",
|
||||||
|
url: "/apis/group/version/watch/resource/name?fieldSelector=metadata.name=foo&labelSelector=foo=bar",
|
||||||
|
expectedName: "name",
|
||||||
|
expectedVerb: "watch",
|
||||||
|
expectedFieldSelector: "",
|
||||||
|
expectedLabelSelector: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver := newTestRequestInfoResolver()
|
resolver := newTestRequestInfoResolver()
|
||||||
|
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
req, _ := http.NewRequest("GET", tc.url, nil)
|
req, _ := http.NewRequest(tc.method, tc.url, nil)
|
||||||
|
|
||||||
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -261,5 +329,11 @@ func TestFieldSelectorParsing(t *testing.T) {
|
||||||
if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a {
|
if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a {
|
||||||
t.Errorf("%s: expected verb %v, actual %v", tc.name, e, a)
|
t.Errorf("%s: expected verb %v, actual %v", tc.name, e, a)
|
||||||
}
|
}
|
||||||
|
if e, a := tc.expectedFieldSelector, apiRequestInfo.FieldSelector; e != a {
|
||||||
|
t.Errorf("%s: expected fieldSelector %v, actual %v", tc.name, e, a)
|
||||||
|
}
|
||||||
|
if e, a := tc.expectedLabelSelector, apiRequestInfo.LabelSelector; e != a {
|
||||||
|
t.Errorf("%s: expected labelSelector %v, actual %v", tc.name, e, a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,13 @@ const (
|
||||||
// Enables serving watch requests in separate goroutines.
|
// Enables serving watch requests in separate goroutines.
|
||||||
APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine"
|
APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine"
|
||||||
|
|
||||||
|
// owner: @deads2k
|
||||||
|
// kep: https://kep.k8s.io/4601
|
||||||
|
// alpha: v1.31
|
||||||
|
//
|
||||||
|
// Allows authorization to use field and label selectors.
|
||||||
|
AuthorizeWithSelectors featuregate.Feature = "AuthorizeWithSelectors"
|
||||||
|
|
||||||
// owner: @cici37 @jpbetz
|
// owner: @cici37 @jpbetz
|
||||||
// kep: http://kep.k8s.io/3488
|
// kep: http://kep.k8s.io/3488
|
||||||
// alpha: v1.26
|
// alpha: v1.26
|
||||||
|
|
@ -358,6 +365,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||||
|
|
||||||
APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
|
APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
|
AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
|
||||||
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
|
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
|
||||||
|
|
||||||
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
|
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,15 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &authorizationv1.ResourceAttributes{
|
return &authorizationv1.ResourceAttributes{
|
||||||
Namespace: in.Namespace,
|
Namespace: in.Namespace,
|
||||||
Verb: in.Verb,
|
Verb: in.Verb,
|
||||||
Group: in.Group,
|
Group: in.Group,
|
||||||
Version: in.Version,
|
Version: in.Version,
|
||||||
Resource: in.Resource,
|
Resource: in.Resource,
|
||||||
Subresource: in.Subresource,
|
Subresource: in.Subresource,
|
||||||
Name: in.Name,
|
Name: in.Name,
|
||||||
|
FieldSelector: in.FieldSelector,
|
||||||
|
LabelSelector: in.LabelSelector,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import (
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/selection"
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
|
@ -40,7 +41,7 @@ import (
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
||||||
"k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
|
"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
|
||||||
|
|
@ -196,15 +197,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr.IsResourceRequest() {
|
if attr.IsResourceRequest() {
|
||||||
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
|
r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
|
||||||
Namespace: attr.GetNamespace(),
|
|
||||||
Verb: attr.GetVerb(),
|
|
||||||
Group: attr.GetAPIGroup(),
|
|
||||||
Version: attr.GetAPIVersion(),
|
|
||||||
Resource: attr.GetResource(),
|
|
||||||
Subresource: attr.GetSubresource(),
|
|
||||||
Name: attr.GetName(),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
|
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
|
||||||
Path: attr.GetPath(),
|
Path: attr.GetPath(),
|
||||||
|
|
@ -212,7 +205,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// skipping match when feature is not enabled
|
// skipping match when feature is not enabled
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration) {
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
|
||||||
// Process Match Conditions before calling the webhook
|
// Process Match Conditions before calling the webhook
|
||||||
matches, err := w.match(ctx, r)
|
matches, err := w.match(ctx, r)
|
||||||
// If at least one matchCondition evaluates to an error (but none are FALSE):
|
// If at least one matchCondition evaluates to an error (but none are FALSE):
|
||||||
|
|
@ -305,6 +298,109 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceAttributesFrom(attr authorizer.Attributes) *authorizationv1.ResourceAttributes {
|
||||||
|
ret := &authorizationv1.ResourceAttributes{
|
||||||
|
Namespace: attr.GetNamespace(),
|
||||||
|
Verb: attr.GetVerb(),
|
||||||
|
Group: attr.GetAPIGroup(),
|
||||||
|
Version: attr.GetAPIVersion(),
|
||||||
|
Resource: attr.GetResource(),
|
||||||
|
Subresource: attr.GetSubresource(),
|
||||||
|
Name: attr.GetName(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
|
||||||
|
// If we are able to get any requirements while parsing selectors, use them, even if there's an error.
|
||||||
|
// This is because selectors only narrow, so if a subset of selector requirements are available, the request can be allowed.
|
||||||
|
if selectorRequirements, _ := fieldSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
|
||||||
|
ret.FieldSelector = &authorizationv1.FieldSelectorAttributes{
|
||||||
|
Requirements: selectorRequirements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectorRequirements, _ := labelSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
|
||||||
|
ret.LabelSelector = &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: selectorRequirements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.FieldSelectorRequirement, error) {
|
||||||
|
requirements, getFieldSelectorErr := attr.GetFieldSelector()
|
||||||
|
if len(requirements) == 0 {
|
||||||
|
return nil, getFieldSelectorErr
|
||||||
|
}
|
||||||
|
|
||||||
|
retRequirements := []metav1.FieldSelectorRequirement{}
|
||||||
|
for _, requirement := range requirements {
|
||||||
|
retRequirement := metav1.FieldSelectorRequirement{}
|
||||||
|
switch {
|
||||||
|
case requirement.Operator == selection.Equals || requirement.Operator == selection.DoubleEquals || requirement.Operator == selection.In:
|
||||||
|
retRequirement.Operator = metav1.FieldSelectorOpIn
|
||||||
|
retRequirement.Key = requirement.Field
|
||||||
|
retRequirement.Values = []string{requirement.Value}
|
||||||
|
case requirement.Operator == selection.NotEquals || requirement.Operator == selection.NotIn:
|
||||||
|
retRequirement.Operator = metav1.FieldSelectorOpNotIn
|
||||||
|
retRequirement.Key = requirement.Field
|
||||||
|
retRequirement.Values = []string{requirement.Value}
|
||||||
|
default:
|
||||||
|
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
|
||||||
|
// for authorization since the resulting check will only be as broad or broader than the intended.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
retRequirements = append(retRequirements, retRequirement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(retRequirements) == 0 {
|
||||||
|
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
|
||||||
|
// unrestricted action.
|
||||||
|
return nil, getFieldSelectorErr
|
||||||
|
}
|
||||||
|
return retRequirements, getFieldSelectorErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.LabelSelectorRequirement, error) {
|
||||||
|
requirements, getLabelSelectorErr := attr.GetLabelSelector()
|
||||||
|
if len(requirements) == 0 {
|
||||||
|
return nil, getLabelSelectorErr
|
||||||
|
}
|
||||||
|
|
||||||
|
retRequirements := []metav1.LabelSelectorRequirement{}
|
||||||
|
for _, requirement := range requirements {
|
||||||
|
retRequirement := metav1.LabelSelectorRequirement{
|
||||||
|
Key: requirement.Key(),
|
||||||
|
}
|
||||||
|
if values := requirement.ValuesUnsorted(); len(values) > 0 {
|
||||||
|
retRequirement.Values = values
|
||||||
|
}
|
||||||
|
switch requirement.Operator() {
|
||||||
|
case selection.Equals, selection.DoubleEquals, selection.In:
|
||||||
|
retRequirement.Operator = metav1.LabelSelectorOpIn
|
||||||
|
case selection.NotEquals, selection.NotIn:
|
||||||
|
retRequirement.Operator = metav1.LabelSelectorOpNotIn
|
||||||
|
case selection.Exists:
|
||||||
|
retRequirement.Operator = metav1.LabelSelectorOpExists
|
||||||
|
case selection.DoesNotExist:
|
||||||
|
retRequirement.Operator = metav1.LabelSelectorOpDoesNotExist
|
||||||
|
default:
|
||||||
|
// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
|
||||||
|
// for authorization since the resulting check will only be as broad or broader than the intended.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
retRequirements = append(retRequirements, retRequirement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(retRequirements) == 0 {
|
||||||
|
// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
|
||||||
|
// unrestricted action.
|
||||||
|
return nil, getLabelSelectorErr
|
||||||
|
}
|
||||||
|
return retRequirements, getLabelSelectorErr
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: need to finish the method to get the rules when using webhook mode
|
// TODO: need to finish the method to get the rules when using webhook mode
|
||||||
func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
|
func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
|
||||||
var (
|
var (
|
||||||
|
|
@ -475,13 +571,15 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &authorizationv1beta1.ResourceAttributes{
|
return &authorizationv1beta1.ResourceAttributes{
|
||||||
Namespace: in.Namespace,
|
Namespace: in.Namespace,
|
||||||
Verb: in.Verb,
|
Verb: in.Verb,
|
||||||
Group: in.Group,
|
Group: in.Group,
|
||||||
Version: in.Version,
|
Version: in.Version,
|
||||||
Resource: in.Resource,
|
Resource: in.Resource,
|
||||||
Subresource: in.Subresource,
|
Subresource: in.Subresource,
|
||||||
Name: in.Name,
|
Name: in.Name,
|
||||||
|
FieldSelector: in.FieldSelector,
|
||||||
|
LabelSelector: in.LabelSelector,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/selection"
|
||||||
|
"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 mustLabelRequirement(selector string) labels.Requirements {
|
||||||
|
ret, err := labels.Parse(selector)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
requirements, _ := ret.Requirements()
|
||||||
|
return requirements
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resourceAttributesFrom(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
attr authorizer.Attributes
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *authorizationv1.ResourceAttributes
|
||||||
|
enableAuthorizationSelector bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "field selector: don't parse when disabled",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: don't parse when disabled",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field selector: ignore error",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: errors.New("failed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
FieldSelector: &authorizationv1.FieldSelectorAttributes{
|
||||||
|
Requirements: []metav1.FieldSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: ignore error",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz)"),
|
||||||
|
LabelSelectorParsingErr: errors.New("failed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
LabelSelector: &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: []metav1.LabelSelectorRequirement{{Key: "foo", Operator: "In", Values: []string{"bar", "baz"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field selector: equals, double equals, in",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
{Operator: selection.Equals, Field: "foo", Value: "bar"},
|
||||||
|
{Operator: selection.DoubleEquals, Field: "one", Value: "two"},
|
||||||
|
{Operator: selection.In, Field: "apple", Value: "banana"},
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
FieldSelector: &authorizationv1.FieldSelectorAttributes{
|
||||||
|
Requirements: []metav1.FieldSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "one",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"two"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "apple",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"banana"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field selector: not equals, not in",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
{Operator: selection.NotEquals, Field: "foo", Value: "bar"},
|
||||||
|
{Operator: selection.NotIn, Field: "apple", Value: "banana"},
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
FieldSelector: &authorizationv1.FieldSelectorAttributes{
|
||||||
|
Requirements: []metav1.FieldSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "apple",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"banana"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field selector: unknown operator skipped",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
{Operator: selection.NotEquals, Field: "foo", Value: "bar"},
|
||||||
|
{Operator: selection.Operator("bad"), Field: "apple", Value: "banana"},
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
FieldSelector: &authorizationv1.FieldSelectorAttributes{
|
||||||
|
Requirements: []metav1.FieldSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field selector: no requirements has no fieldselector",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
{Operator: selection.Operator("bad"), Field: "apple", Value: "banana"},
|
||||||
|
},
|
||||||
|
FieldSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: in, equals, double equals",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo in (bar,baz), one=two, apple==banana"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
LabelSelector: &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: []metav1.LabelSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "apple",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"banana"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"bar", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "one",
|
||||||
|
Operator: "In",
|
||||||
|
Values: []string{"two"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: not in, not equals",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo notin (bar,baz), one!=two"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
LabelSelector: &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: []metav1.LabelSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"bar", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "one",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"two"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: exists, not exists",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo, !one"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
LabelSelector: &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: []metav1.LabelSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "Exists",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "one",
|
||||||
|
Operator: "DoesNotExist",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: unknown operator skipped",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("foo != bar, apple > 1"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{
|
||||||
|
LabelSelector: &authorizationv1.LabelSelectorAttributes{
|
||||||
|
Requirements: []metav1.LabelSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "foo",
|
||||||
|
Operator: "NotIn",
|
||||||
|
Values: []string{"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label selector: no requirements has no labelselector",
|
||||||
|
args: args{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
LabelSelectorRequirements: mustLabelRequirement("apple > 1"),
|
||||||
|
LabelSelectorParsingErr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &authorizationv1.ResourceAttributes{},
|
||||||
|
enableAuthorizationSelector: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.enableAuthorizationSelector {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := resourceAttributesFrom(tt.args.attr); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("resourceAttributesFrom() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue