mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 22:17:14 +00:00
Improve error messaging for validating admission policy authz
This commit is contained in:
parent
c0f9c81338
commit
1ad6fd7a0f
@ -20,10 +20,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
userInfo user.Info
|
userInfo user.Info
|
||||||
|
obj *admissionregistration.ValidatingAdmissionPolicy
|
||||||
auth AuthFunc
|
auth AuthFunc
|
||||||
resourceResolver resolver.ResourceResolverFunc
|
resourceResolver resolver.ResourceResolverFunc
|
||||||
expectErr bool
|
expectErr bool
|
||||||
@ -39,6 +42,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
name: "superuser",
|
name: "superuser",
|
||||||
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
||||||
expectErr: false, // success despite always-denying authorizer
|
expectErr: false, // success despite always-denying authorizer
|
||||||
|
obj: validValidatingAdmissionPolicy(),
|
||||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
return authorizer.DecisionDeny, "", nil
|
return authorizer.DecisionDeny, "", nil
|
||||||
},
|
},
|
||||||
@ -46,6 +50,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "authorized",
|
name: "authorized",
|
||||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
obj: validValidatingAdmissionPolicy(),
|
||||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
if a.GetResource() == "replicalimits" {
|
if a.GetResource() == "replicalimits" {
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
@ -64,6 +69,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "denied",
|
name: "denied",
|
||||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
obj: validValidatingAdmissionPolicy(),
|
||||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
if a.GetResource() == "configmaps" {
|
if a.GetResource() == "configmaps" {
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
@ -79,22 +85,36 @@ func TestAuthorization(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "param not found",
|
||||||
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
obj: validValidatingAdmissionPolicy(),
|
||||||
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
|
if a.GetResource() == "replicalimits" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionDeny, "", nil
|
||||||
|
},
|
||||||
|
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}}
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
strategy := NewStrategy(tc.auth, tc.resourceResolver)
|
strategy := NewStrategy(tc.auth, tc.resourceResolver)
|
||||||
t.Run("create", func(t *testing.T) {
|
t.Run("create", func(t *testing.T) {
|
||||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||||
errs := strategy.Validate(ctx, validValidatingAdmissionPolicy())
|
errs := strategy.Validate(ctx, tc.obj)
|
||||||
if len(errs) > 0 != tc.expectErr {
|
if len(errs) > 0 != tc.expectErr {
|
||||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("update", func(t *testing.T) {
|
t.Run("update", func(t *testing.T) {
|
||||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||||
obj := validValidatingAdmissionPolicy()
|
objWithUpdatedParamKind := tc.obj.DeepCopy()
|
||||||
objWithUpdatedParamKind := obj.DeepCopy()
|
|
||||||
objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1"
|
objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1"
|
||||||
errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind)
|
errs := strategy.ValidateUpdate(ctx, tc.obj, objWithUpdatedParamKind)
|
||||||
if len(errs) > 0 != tc.expectErr {
|
if len(errs) > 0 != tc.expectErr {
|
||||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
@ -201,7 +202,7 @@ func newValidatingAdmissionPolicy(name string) *admissionregistration.Validating
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
return newStorage(t, nil, nil)
|
return newStorage(t, nil, replicaLimitsResolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
@ -225,3 +226,11 @@ func TestCategories(t *testing.T) {
|
|||||||
expected := []string{"api-extensions"}
|
expected := []string{"api-extensions"}
|
||||||
registrytest.AssertCategories(t, storage, expected)
|
registrytest.AssertCategories(t, storage, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "rules.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "replicalimits",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -20,12 +20,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidatingAdmissionPolicyStrategy(t *testing.T) {
|
func TestValidatingAdmissionPolicyStrategy(t *testing.T) {
|
||||||
strategy := NewStrategy(nil, nil)
|
strategy := NewStrategy(nil, replicaLimitsResolver)
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
if strategy.NamespaceScoped() {
|
if strategy.NamespaceScoped() {
|
||||||
t.Error("ValidatingAdmissionPolicy strategy must be cluster scoped")
|
t.Error("ValidatingAdmissionPolicy strategy must be cluster scoped")
|
||||||
@ -49,6 +51,15 @@ func TestValidatingAdmissionPolicyStrategy(t *testing.T) {
|
|||||||
t.Errorf("Expected a validation error")
|
t.Errorf("Expected a validation error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "rules.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "replicalimits",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func validValidatingAdmissionPolicy() *admissionregistration.ValidatingAdmissionPolicy {
|
func validValidatingAdmissionPolicy() *admissionregistration.ValidatingAdmissionPolicy {
|
||||||
ignore := admissionregistration.Ignore
|
ignore := admissionregistration.Ignore
|
||||||
return &admissionregistration.ValidatingAdmissionPolicy{
|
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
@ -55,7 +55,10 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.ValidatingAdmissionPolicyBinding) error {
|
func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.ValidatingAdmissionPolicyBinding) error {
|
||||||
if v.authorizer == nil || v.resourceResolver == nil || binding.Spec.ParamRef == nil {
|
if v.resourceResolver == nil {
|
||||||
|
return fmt.Errorf(`unexpected internal error: resourceResolver is nil`)
|
||||||
|
}
|
||||||
|
if v.authorizer == nil || binding.Spec.ParamRef == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,13 +75,21 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context
|
|||||||
// default to requiring permissions on all group/version/resources
|
// default to requiring permissions on all group/version/resources
|
||||||
resource, apiGroup, apiVersion := "*", "*", "*"
|
resource, apiGroup, apiVersion := "*", "*", "*"
|
||||||
|
|
||||||
if policy, err := v.policyGetter.GetValidatingAdmissionPolicy(ctx, binding.Spec.PolicyName); err == nil && policy.Spec.ParamKind != nil {
|
var policyErr, gvParseErr, gvrResolveErr error
|
||||||
|
|
||||||
|
var policy *admissionregistration.ValidatingAdmissionPolicy
|
||||||
|
policy, policyErr = v.policyGetter.GetValidatingAdmissionPolicy(ctx, binding.Spec.PolicyName)
|
||||||
|
if policyErr == nil && policy.Spec.ParamKind != nil {
|
||||||
paramKind := policy.Spec.ParamKind
|
paramKind := policy.Spec.ParamKind
|
||||||
if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil {
|
var gv schema.GroupVersion
|
||||||
|
gv, gvParseErr = schema.ParseGroupVersion(paramKind.APIVersion)
|
||||||
|
if gvParseErr == nil {
|
||||||
// we only need to authorize the parsed group/version
|
// we only need to authorize the parsed group/version
|
||||||
apiGroup = gv.Group
|
apiGroup = gv.Group
|
||||||
apiVersion = gv.Version
|
apiVersion = gv.Version
|
||||||
if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil {
|
var gvr schema.GroupVersionResource
|
||||||
|
gvr, gvrResolveErr = v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind))
|
||||||
|
if gvrResolveErr == nil {
|
||||||
// we only need to authorize the resolved resource
|
// we only need to authorize the resolved resource
|
||||||
resource = gvr.Resource
|
resource = gvr.Resource
|
||||||
}
|
}
|
||||||
@ -107,9 +118,18 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context
|
|||||||
|
|
||||||
d, _, err := v.authorizer.Authorize(ctx, attrs)
|
d, _, err := v.authorizer.Authorize(ctx, attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf(`failed to authorize request: %w`, err)
|
||||||
}
|
}
|
||||||
if d != authorizer.DecisionAllow {
|
if d != authorizer.DecisionAllow {
|
||||||
|
if policyErr != nil {
|
||||||
|
return fmt.Errorf(`unable to get policy %s to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, binding.Spec.PolicyName, user, verb)
|
||||||
|
}
|
||||||
|
if gvParseErr != nil {
|
||||||
|
return fmt.Errorf(`unable to parse paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb)
|
||||||
|
}
|
||||||
|
if gvrResolveErr != nil {
|
||||||
|
return fmt.Errorf(`unable to resolve paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb)
|
||||||
|
}
|
||||||
return fmt.Errorf(`user %v does not have "%v" permission on the object referenced by paramRef`, user, verb)
|
return fmt.Errorf(`user %v does not have "%v" permission on the object referenced by paramRef`, user, verb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,12 @@ package validatingadmissionpolicybinding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
@ -31,17 +35,16 @@ import (
|
|||||||
|
|
||||||
func TestAuthorization(t *testing.T) {
|
func TestAuthorization(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
userInfo user.Info
|
userInfo user.Info
|
||||||
auth AuthFunc
|
auth AuthFunc
|
||||||
policyGetter PolicyGetterFunc
|
policyGetter PolicyGetterFunc
|
||||||
resourceResolver resolver.ResourceResolverFunc
|
resourceResolver resolver.ResourceResolverFunc
|
||||||
expectErr bool
|
expectErrContains string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "superuser",
|
name: "superuser", // success despite always-denying authorizer
|
||||||
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
||||||
expectErr: false, // success despite always-denying authorizer
|
|
||||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
return authorizer.DecisionDeny, "", nil
|
return authorizer.DecisionDeny, "", nil
|
||||||
},
|
},
|
||||||
@ -70,7 +73,6 @@ func TestAuthorization(t *testing.T) {
|
|||||||
Resource: "configmaps",
|
Resource: "configmaps",
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
expectErr: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "denied",
|
name: "denied",
|
||||||
@ -96,7 +98,76 @@ func TestAuthorization(t *testing.T) {
|
|||||||
Resource: "params",
|
Resource: "params",
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
expectErr: true,
|
expectErrContains: "permission on the object referenced by paramRef",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unable to parse paramRef",
|
||||||
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
|
if a.GetResource() == "configmaps" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionDeny, "", nil
|
||||||
|
},
|
||||||
|
policyGetter: func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||||
|
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "invalid"},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "configmaps",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
expectErrContains: "unable to parse paramKind &{foo.example.com/v1 Params} to determine minimum required permissions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unable to resolve param",
|
||||||
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
|
if a.GetResource() == "configmaps" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionDeny, "", nil
|
||||||
|
},
|
||||||
|
policyGetter: func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||||
|
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}}
|
||||||
|
},
|
||||||
|
expectErrContains: "unable to resolve paramKind &{foo.example.com/v1 Params} to determine minimum required permissions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unable to get policy",
|
||||||
|
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||||
|
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
|
if a.GetResource() == "configmaps" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionDeny, "", nil
|
||||||
|
},
|
||||||
|
policyGetter: func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||||
|
return nil, fmt.Errorf("no such policy")
|
||||||
|
},
|
||||||
|
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "configmaps",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
expectErrContains: "unable to get policy replicalimit-policy.example.com to determine minimum required permissions",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@ -105,8 +176,8 @@ func TestAuthorization(t *testing.T) {
|
|||||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||||
for _, obj := range validPolicyBindings() {
|
for _, obj := range validPolicyBindings() {
|
||||||
errs := strategy.Validate(ctx, obj)
|
errs := strategy.Validate(ctx, obj)
|
||||||
if len(errs) > 0 != tc.expectErr {
|
if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) {
|
||||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -140,8 +211,8 @@ func TestAuthorization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef)
|
errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef)
|
||||||
if len(errs) > 0 != tc.expectErr {
|
if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) {
|
||||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
@ -230,7 +231,7 @@ func newPolicyBinding(name string) *admissionregistration.ValidatingAdmissionPol
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
return newStorage(t, nil, nil, nil)
|
return newStorage(t, nil, nil, replicaLimitsResolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
@ -254,3 +255,11 @@ func TestCategories(t *testing.T) {
|
|||||||
expected := []string{"api-extensions"}
|
expected := []string{"api-extensions"}
|
||||||
registrytest.AssertCategories(t, storage, expected)
|
registrytest.AssertCategories(t, storage, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "rules.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "replicalimits",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -20,13 +20,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPolicyBindingStrategy(t *testing.T) {
|
func TestPolicyBindingStrategy(t *testing.T) {
|
||||||
strategy := NewStrategy(nil, nil, nil)
|
strategy := NewStrategy(nil, nil, replicaLimitsResolver)
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
if strategy.NamespaceScoped() {
|
if strategy.NamespaceScoped() {
|
||||||
t.Error("PolicyBinding strategy must be cluster scoped")
|
t.Error("PolicyBinding strategy must be cluster scoped")
|
||||||
@ -52,6 +54,14 @@ func TestPolicyBindingStrategy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: "rules.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "replicalimits",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func validPolicyBindings() []*admissionregistration.ValidatingAdmissionPolicyBinding {
|
func validPolicyBindings() []*admissionregistration.ValidatingAdmissionPolicyBinding {
|
||||||
denyAction := admissionregistration.DenyAction
|
denyAction := admissionregistration.DenyAction
|
||||||
return []*admissionregistration.ValidatingAdmissionPolicyBinding{
|
return []*admissionregistration.ValidatingAdmissionPolicyBinding{
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package validating
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type authzResult struct {
|
|
||||||
authorized authorizer.Decision
|
|
||||||
reason string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type cachingAuthorizer struct {
|
|
||||||
authorizer authorizer.Authorizer
|
|
||||||
decisions map[string]authzResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
|
|
||||||
return &cachingAuthorizer{
|
|
||||||
authorizer: in,
|
|
||||||
decisions: make(map[string]authzResult),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The attribute accessors known to cache key construction. If this fails to compile, the cache
|
|
||||||
// implementation may need to be updated.
|
|
||||||
var _ authorizer.Attributes = (interface {
|
|
||||||
GetUser() user.Info
|
|
||||||
GetVerb() string
|
|
||||||
IsReadOnly() bool
|
|
||||||
GetNamespace() string
|
|
||||||
GetResource() string
|
|
||||||
GetSubresource() string
|
|
||||||
GetName() string
|
|
||||||
GetAPIGroup() string
|
|
||||||
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
|
|
||||||
// implementation may need to be updated.
|
|
||||||
var _ user.Info = (interface {
|
|
||||||
GetName() string
|
|
||||||
GetUID() string
|
|
||||||
GetGroups() []string
|
|
||||||
GetExtra() map[string][]string
|
|
||||||
})(nil)
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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 {
|
|
||||||
di := &user.DefaultInfo{
|
|
||||||
Name: u.GetName(),
|
|
||||||
UID: u.GetUID(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Differently-ordered groups or extras could cause otherwise-equivalent checks to
|
|
||||||
// have distinct cache keys.
|
|
||||||
if groups := u.GetGroups(); len(groups) > 0 {
|
|
||||||
di.Groups = make([]string, len(groups))
|
|
||||||
copy(di.Groups, groups)
|
|
||||||
sort.Strings(di.Groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
if extra := u.GetExtra(); len(extra) > 0 {
|
|
||||||
di.Extra = make(map[string][]string, len(extra))
|
|
||||||
for k, vs := range extra {
|
|
||||||
vdupe := make([]string, len(vs))
|
|
||||||
copy(vdupe, vs)
|
|
||||||
sort.Strings(vdupe)
|
|
||||||
di.Extra[k] = vdupe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serializableAttributes.User = di
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil {
|
|
||||||
return authorizer.DecisionNoOpinion, "", err
|
|
||||||
}
|
|
||||||
key := b.String()
|
|
||||||
|
|
||||||
if cached, ok := ca.decisions[key]; ok {
|
|
||||||
return cached.authorized, cached.reason, cached.err
|
|
||||||
}
|
|
||||||
|
|
||||||
authorized, reason, err := ca.authorizer.Authorize(ctx, a)
|
|
||||||
|
|
||||||
ca.decisions[key] = authzResult{
|
|
||||||
authorized: authorized,
|
|
||||||
reason: reason,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
|
|
||||||
return authorized, reason, err
|
|
||||||
}
|
|
@ -1,523 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package 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
|
|
||||||
error error
|
|
||||||
}
|
|
||||||
|
|
||||||
type invocation struct {
|
|
||||||
attributes authorizer.Attributes
|
|
||||||
expected result
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
calls []invocation
|
|
||||||
backend []result
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "hit",
|
|
||||||
calls: []invocation{
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{Name: "test name"},
|
|
||||||
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",
|
|
||||||
error: fmt.Errorf("test error"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backend: []result{
|
|
||||||
{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason",
|
|
||||||
error: fmt.Errorf("test error"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hit with differently-ordered groups",
|
|
||||||
calls: []invocation{
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{
|
|
||||||
Groups: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason",
|
|
||||||
error: fmt.Errorf("test error"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{
|
|
||||||
Groups: []string{"c", "b", "a"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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: "hit with differently-ordered extra",
|
|
||||||
calls: []invocation{
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{
|
|
||||||
Extra: map[string][]string{
|
|
||||||
"k": {"a", "b", "c"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason",
|
|
||||||
error: fmt.Errorf("test error"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{
|
|
||||||
Extra: map[string][]string{
|
|
||||||
"k": {"c", "b", "a"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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: "miss due to different name",
|
|
||||||
calls: []invocation{
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{Name: "alpha"},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason alpha",
|
|
||||||
error: fmt.Errorf("test error alpha"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{Name: "beta"},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionDeny,
|
|
||||||
reason: "test reason beta",
|
|
||||||
error: fmt.Errorf("test error beta"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backend: []result{
|
|
||||||
{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason alpha",
|
|
||||||
error: fmt.Errorf("test error alpha"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decision: authorizer.DecisionDeny,
|
|
||||||
reason: "test reason beta",
|
|
||||||
error: fmt.Errorf("test error beta"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "miss due to different user",
|
|
||||||
calls: []invocation{
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{Name: "alpha"},
|
|
||||||
},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason alpha",
|
|
||||||
error: fmt.Errorf("test error alpha"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: authorizer.AttributesRecord{
|
|
||||||
User: &user.DefaultInfo{Name: "beta"},
|
|
||||||
},
|
|
||||||
expected: result{
|
|
||||||
decision: authorizer.DecisionDeny,
|
|
||||||
reason: "test reason beta",
|
|
||||||
error: fmt.Errorf("test error beta"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backend: []result{
|
|
||||||
{
|
|
||||||
decision: authorizer.DecisionAllow,
|
|
||||||
reason: "test reason alpha",
|
|
||||||
error: fmt.Errorf("test error alpha"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decision: authorizer.DecisionDeny,
|
|
||||||
reason: "test reason beta",
|
|
||||||
error: fmt.Errorf("test error beta"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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
|
|
||||||
frontend := newCachingAuthorizer(func() authorizer.Authorizer {
|
|
||||||
return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
|
|
||||||
if misses >= len(tc.backend) {
|
|
||||||
t.Fatalf("got more than expected %d backend invocations", len(tc.backend))
|
|
||||||
}
|
|
||||||
result := tc.backend[misses]
|
|
||||||
misses++
|
|
||||||
return result.decision, result.reason, result.error
|
|
||||||
})
|
|
||||||
}())
|
|
||||||
|
|
||||||
for i, invocation := range tc.calls {
|
|
||||||
decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes)
|
|
||||||
if decision != invocation.expected.decision {
|
|
||||||
t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision)
|
|
||||||
}
|
|
||||||
if reason != invocation.expected.reason {
|
|
||||||
t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason)
|
|
||||||
}
|
|
||||||
if err.Error() != invocation.expected.error.Error() {
|
|
||||||
t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tc.backend) > misses {
|
|
||||||
t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -41,13 +41,13 @@ import (
|
|||||||
// validator implements the Validator interface
|
// validator implements the Validator interface
|
||||||
type validator struct {
|
type validator struct {
|
||||||
celMatcher matchconditions.Matcher
|
celMatcher matchconditions.Matcher
|
||||||
validationFilter cel.Filter
|
validationFilter cel.ConditionEvaluator
|
||||||
auditAnnotationFilter cel.Filter
|
auditAnnotationFilter cel.ConditionEvaluator
|
||||||
messageFilter cel.Filter
|
messageFilter cel.ConditionEvaluator
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
|
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
|
||||||
return &validator{
|
return &validator{
|
||||||
celMatcher: celMatcher,
|
celMatcher: celMatcher,
|
||||||
validationFilter: validationFilter,
|
validationFilter: validationFilter,
|
||||||
|
@ -41,7 +41,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/cel/environment"
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ cel.Filter = &fakeCelFilter{}
|
var _ cel.ConditionEvaluator = &fakeCelFilter{}
|
||||||
|
|
||||||
type fakeCelFilter struct {
|
type fakeCelFilter struct {
|
||||||
evaluations []cel.EvaluationResult
|
evaluations []cel.EvaluationResult
|
||||||
@ -932,8 +932,8 @@ func TestContextCanceled(t *testing.T) {
|
|||||||
|
|
||||||
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
|
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
|
||||||
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
||||||
fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
fc := cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||||
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
|
f := fc.CompileCondition([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
|
||||||
v := validator{
|
v := validator{
|
||||||
failPolicy: &fail,
|
failPolicy: &fail,
|
||||||
celMatcher: &fakeCELMatcher{matches: true},
|
celMatcher: &fakeCELMatcher{matches: true},
|
||||||
|
Loading…
Reference in New Issue
Block a user