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"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
|
@ -58,6 +60,8 @@ var _ authorizer.Attributes = (interface {
|
|||
GetAPIVersion() string
|
||||
IsResourceRequest() bool
|
||||
GetPath() string
|
||||
GetFieldSelector() (fields.Requirements, error)
|
||||
GetLabelSelector() (labels.Requirements, error)
|
||||
})(nil)
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
serializableAttributes := authorizer.AttributesRecord{
|
||||
Verb: a.GetVerb(),
|
||||
Namespace: a.GetNamespace(),
|
||||
APIGroup: a.GetAPIGroup(),
|
||||
APIVersion: a.GetAPIVersion(),
|
||||
Resource: a.GetResource(),
|
||||
Subresource: a.GetSubresource(),
|
||||
Name: a.GetName(),
|
||||
ResourceRequest: a.IsResourceRequest(),
|
||||
Path: a.GetPath(),
|
||||
type SerializableAttributes struct {
|
||||
authorizer.AttributesRecord
|
||||
LabelSelector string
|
||||
}
|
||||
|
||||
serializableAttributes := SerializableAttributes{
|
||||
AttributesRecord: authorizer.AttributesRecord{
|
||||
Verb: a.GetVerb(),
|
||||
Namespace: a.GetNamespace(),
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -18,14 +18,31 @@ package validating
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"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 {
|
||||
ret, err := labels.Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
retRequirements, _ /*selectable*/ := ret.Requirements()
|
||||
return retRequirements
|
||||
}
|
||||
|
||||
func TestCachingAuthorizer(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||
|
||||
type result struct {
|
||||
decision authorizer.Decision
|
||||
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) {
|
||||
var misses int
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
|
|
@ -62,6 +64,16 @@ type Attributes interface {
|
|||
|
||||
// GetPath returns the path of the request
|
||||
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
|
||||
|
|
@ -100,6 +112,11 @@ type AttributesRecord struct {
|
|||
Name string
|
||||
ResourceRequest bool
|
||||
Path string
|
||||
|
||||
FieldSelectorRequirements fields.Requirements
|
||||
FieldSelectorParsingErr error
|
||||
LabelSelectorRequirements labels.Requirements
|
||||
LabelSelectorParsingErr error
|
||||
}
|
||||
|
||||
func (a AttributesRecord) GetUser() user.Info {
|
||||
|
|
@ -146,6 +163,14 @@ func (a AttributesRecord) GetPath() string {
|
|||
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
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import (
|
|||
"net/http"
|
||||
"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/apimachinery/pkg/runtime"
|
||||
|
|
@ -117,5 +122,31 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error)
|
|||
attribs.Namespace = requestInfo.Namespace
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ package filters
|
|||
import (
|
||||
"context"
|
||||
"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/httptest"
|
||||
"reflect"
|
||||
|
|
@ -34,10 +40,16 @@ import (
|
|||
)
|
||||
|
||||
func TestGetAuthorizerAttributes(t *testing.T) {
|
||||
basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testcases := map[string]struct {
|
||||
Verb string
|
||||
Path string
|
||||
ExpectedAttributes *authorizer.AttributesRecord
|
||||
Verb string
|
||||
Path string
|
||||
ExpectedAttributes *authorizer.AttributesRecord
|
||||
EnableAuthorizationSelector bool
|
||||
}{
|
||||
"non-resource root": {
|
||||
Verb: "POST",
|
||||
|
|
@ -102,26 +114,122 @@ func TestGetAuthorizerAttributes(t *testing.T) {
|
|||
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 {
|
||||
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
t.Run(k, func(t *testing.T) {
|
||||
if tc.EnableAuthorizationSelector {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||
}
|
||||
|
||||
var attribs authorizer.Attributes
|
||||
var err error
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
attribs, err = GetAuthorizerAttributes(ctx)
|
||||
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
|
||||
var attribs authorizer.Attributes
|
||||
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"
|
||||
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"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
|
@ -62,6 +64,13 @@ type RequestInfo struct {
|
|||
Name string
|
||||
// Parts are the path parts for the request, always starting with /{resource}/{name}
|
||||
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
|
||||
|
|
@ -77,6 +86,9 @@ var specialVerbsNoSubresources = sets.NewString("proxy")
|
|||
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
|
||||
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
|
||||
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
|
||||
|
||||
|
|
@ -151,6 +163,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
|||
currentParts = currentParts[1:]
|
||||
|
||||
// handle input of form /{specialVerb}/*
|
||||
verbViaPathPrefix := false
|
||||
if specialVerbs.Has(currentParts[0]) {
|
||||
if len(currentParts) < 2 {
|
||||
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]
|
||||
currentParts = currentParts[1:]
|
||||
verbViaPathPrefix = true
|
||||
|
||||
} else {
|
||||
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 len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ 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) {
|
||||
|
|
@ -190,64 +193,129 @@ func newTestRequestInfoResolver() *RequestInfoFactory {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFieldSelectorParsing(t *testing.T) {
|
||||
func TestSelectorParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expectedName string
|
||||
expectedErr error
|
||||
expectedVerb string
|
||||
name string
|
||||
method string
|
||||
url string
|
||||
expectedName string
|
||||
expectedErr error
|
||||
expectedVerb string
|
||||
expectedFieldSelector string
|
||||
expectedLabelSelector string
|
||||
}{
|
||||
{
|
||||
name: "no selector",
|
||||
url: "/apis/group/version/resource",
|
||||
expectedVerb: "list",
|
||||
name: "no selector",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource",
|
||||
expectedVerb: "list",
|
||||
expectedFieldSelector: "",
|
||||
},
|
||||
{
|
||||
name: "metadata.name selector",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1",
|
||||
expectedName: "name1",
|
||||
expectedVerb: "list",
|
||||
name: "metadata.name selector",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1",
|
||||
expectedName: "name1",
|
||||
expectedVerb: "list",
|
||||
expectedFieldSelector: "metadata.name=name1",
|
||||
},
|
||||
{
|
||||
name: "metadata.name selector with watch",
|
||||
url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
|
||||
expectedName: "name1",
|
||||
expectedVerb: "watch",
|
||||
name: "metadata.name selector with watch",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
|
||||
expectedName: "name1",
|
||||
expectedVerb: "watch",
|
||||
expectedFieldSelector: "metadata.name=name1",
|
||||
},
|
||||
{
|
||||
name: "random selector",
|
||||
url: "/apis/group/version/resource?fieldSelector=foo=bar",
|
||||
expectedName: "",
|
||||
expectedVerb: "list",
|
||||
name: "random selector",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
|
||||
expectedName: "",
|
||||
expectedVerb: "list",
|
||||
expectedFieldSelector: "foo=bar",
|
||||
expectedLabelSelector: "baz=qux",
|
||||
},
|
||||
{
|
||||
name: "invalid selector with metadata.name",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
|
||||
expectedName: "",
|
||||
expectedErr: fmt.Errorf("invalid selector"),
|
||||
expectedVerb: "list",
|
||||
name: "invalid selector with metadata.name",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
|
||||
expectedName: "",
|
||||
expectedErr: fmt.Errorf("invalid selector"),
|
||||
expectedVerb: "list",
|
||||
expectedFieldSelector: "metadata.name=name1,foo",
|
||||
},
|
||||
{
|
||||
name: "invalid selector with metadata.name with watch",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
|
||||
expectedName: "",
|
||||
expectedErr: fmt.Errorf("invalid selector"),
|
||||
expectedVerb: "watch",
|
||||
name: "invalid selector with metadata.name with watch",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
|
||||
expectedName: "",
|
||||
expectedErr: fmt.Errorf("invalid selector"),
|
||||
expectedVerb: "watch",
|
||||
expectedFieldSelector: "metadata.name=name1,foo",
|
||||
},
|
||||
{
|
||||
name: "invalid selector with metadata.name with watch false",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
|
||||
expectedName: "",
|
||||
expectedErr: fmt.Errorf("invalid selector"),
|
||||
expectedVerb: "list",
|
||||
name: "invalid selector with metadata.name with watch false",
|
||||
method: "GET",
|
||||
url: "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
|
||||
expectedName: "",
|
||||
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()
|
||||
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||
|
||||
for _, tc := range tests {
|
||||
req, _ := http.NewRequest("GET", tc.url, nil)
|
||||
req, _ := http.NewRequest(tc.method, tc.url, nil)
|
||||
|
||||
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
||||
if err != nil {
|
||||
|
|
@ -261,5 +329,11 @@ func TestFieldSelectorParsing(t *testing.T) {
|
|||
if e, a := tc.expectedVerb, apiRequestInfo.Verb; 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.
|
||||
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
|
||||
// kep: http://kep.k8s.io/3488
|
||||
// alpha: v1.26
|
||||
|
|
@ -358,6 +365,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||
|
||||
APIServingWithRoutine: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
|
||||
|
||||
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
|
||||
|
|
|
|||
|
|
@ -81,13 +81,15 @@ func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.Re
|
|||
return nil
|
||||
}
|
||||
return &authorizationv1.ResourceAttributes{
|
||||
Namespace: in.Namespace,
|
||||
Verb: in.Verb,
|
||||
Group: in.Group,
|
||||
Version: in.Version,
|
||||
Resource: in.Resource,
|
||||
Subresource: in.Subresource,
|
||||
Name: in.Name,
|
||||
Namespace: in.Namespace,
|
||||
Verb: in.Verb,
|
||||
Group: in.Group,
|
||||
Version: in.Version,
|
||||
Resource: in.Resource,
|
||||
Subresource: in.Subresource,
|
||||
Name: in.Name,
|
||||
FieldSelector: in.FieldSelector,
|
||||
LabelSelector: in.LabelSelector,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
|
@ -40,7 +41,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
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"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"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() {
|
||||
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
|
||||
Namespace: attr.GetNamespace(),
|
||||
Verb: attr.GetVerb(),
|
||||
Group: attr.GetAPIGroup(),
|
||||
Version: attr.GetAPIVersion(),
|
||||
Resource: attr.GetResource(),
|
||||
Subresource: attr.GetSubresource(),
|
||||
Name: attr.GetName(),
|
||||
}
|
||||
r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
|
||||
} else {
|
||||
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
|
||||
Path: attr.GetPath(),
|
||||
|
|
@ -212,7 +205,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
|||
}
|
||||
}
|
||||
// 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
|
||||
matches, err := w.match(ctx, r)
|
||||
// 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
|
||||
func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
|
||||
var (
|
||||
|
|
@ -475,13 +571,15 @@ func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.Resourc
|
|||
return nil
|
||||
}
|
||||
return &authorizationv1beta1.ResourceAttributes{
|
||||
Namespace: in.Namespace,
|
||||
Verb: in.Verb,
|
||||
Group: in.Group,
|
||||
Version: in.Version,
|
||||
Resource: in.Resource,
|
||||
Subresource: in.Subresource,
|
||||
Name: in.Name,
|
||||
Namespace: in.Namespace,
|
||||
Verb: in.Verb,
|
||||
Group: in.Group,
|
||||
Version: in.Version,
|
||||
Resource: in.Resource,
|
||||
Subresource: in.Subresource,
|
||||
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