diff --git a/pkg/admission/plugin/webhook/rules/rules.go b/pkg/admission/plugin/webhook/rules/rules.go index 096ab5021..050885323 100644 --- a/pkg/admission/plugin/webhook/rules/rules.go +++ b/pkg/admission/plugin/webhook/rules/rules.go @@ -20,6 +20,8 @@ import ( "strings" "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" ) @@ -31,7 +33,8 @@ type Matcher struct { // Matches returns if the Attr matches the Rule. func (r *Matcher) Matches() bool { - return r.operation() && + return r.scope() && + r.operation() && r.group() && r.version() && r.resource() @@ -50,6 +53,25 @@ func exactOrWildcard(items []string, requested string) bool { return false } +var namespaceResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"} + +func (r *Matcher) scope() bool { + if r.Rule.Scope == nil || *r.Rule.Scope == v1beta1.AllScopes { + return true + } + // attr.GetNamespace() is set to the name of the namespace for requests of the namespace object itself. + switch *r.Rule.Scope { + case v1beta1.NamespacedScope: + // first make sure that we are not requesting a namespace object (namespace objects are cluster-scoped) + return r.Attr.GetResource() != namespaceResource && r.Attr.GetNamespace() != metav1.NamespaceNone + case v1beta1.ClusterScope: + // also return true if the request is for a namespace object (namespace objects are cluster-scoped) + return r.Attr.GetResource() == namespaceResource || r.Attr.GetNamespace() == metav1.NamespaceNone + default: + return false + } +} + func (r *Matcher) group() bool { return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group) } diff --git a/pkg/admission/plugin/webhook/rules/rules_test.go b/pkg/admission/plugin/webhook/rules/rules_test.go index 2827558af..85fba433e 100644 --- a/pkg/admission/plugin/webhook/rules/rules_test.go +++ b/pkg/admission/plugin/webhook/rules/rules_test.go @@ -17,10 +17,12 @@ limitations under the License. package rules import ( + "fmt" "testing" adreg "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" ) @@ -43,6 +45,30 @@ func a(group, version, resource, subresource, name string, operation admission.O ) } +func namespacedAttributes(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes { + return admission.NewAttributesRecord( + nil, nil, + schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource}, + "ns", name, + schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource, + operation, + false, + nil, + ) +} + +func clusterScopedAttributes(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes { + return admission.NewAttributesRecord( + nil, nil, + schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource}, + "", name, + schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource, + operation, + false, + nil, + ) +} + func attrList(a ...admission.Attributes) []admission.Attributes { return a } @@ -299,3 +325,93 @@ func TestResource(t *testing.T) { } } } + +func TestScope(t *testing.T) { + cluster := adreg.ClusterScope + namespace := adreg.NamespacedScope + allscopes := adreg.AllScopes + table := tests{ + "cluster scope": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"*"}, + Scope: &cluster, + }, + }, + match: attrList( + clusterScopedAttributes("g", "v", "r", "", "name", admission.Create), + clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create), + clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + ), + noMatch: attrList( + namespacedAttributes("g", "v", "r", "", "name", admission.Create), + namespacedAttributes("g", "v", "r", "exec", "name", admission.Create), + ), + }, + "namespace scope": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"*"}, + Scope: &namespace, + }, + }, + match: attrList( + namespacedAttributes("g", "v", "r", "", "name", admission.Create), + namespacedAttributes("g", "v", "r", "exec", "name", admission.Create), + ), + noMatch: attrList( + clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + clusterScopedAttributes("g", "v", "r", "", "name", admission.Create), + clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create), + ), + }, + "all scopes": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"*"}, + Scope: &allscopes, + }, + }, + match: attrList( + namespacedAttributes("g", "v", "r", "", "name", admission.Create), + namespacedAttributes("g", "v", "r", "exec", "name", admission.Create), + clusterScopedAttributes("g", "v", "r", "", "name", admission.Create), + clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create), + clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create), + namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create), + ), + noMatch: attrList(), + }, + } + keys := sets.NewString() + for name := range table { + keys.Insert(name) + } + for _, name := range keys.List() { + tt := table[name] + for i, m := range tt.match { + t.Run(fmt.Sprintf("%s_match_%d", name, i), func(t *testing.T) { + r := Matcher{tt.rule, m} + if !r.scope() { + t.Errorf("%v: expected match %#v", name, m) + } + }) + } + for i, m := range tt.noMatch { + t.Run(fmt.Sprintf("%s_nomatch_%d", name, i), func(t *testing.T) { + r := Matcher{tt.rule, m} + if r.scope() { + t.Errorf("%v: expected no match %#v", name, m) + } + }) + } + } +}