diff --git a/config/crd/bases/mutations.gatekeeper.sh_assign.yaml b/config/crd/bases/mutations.gatekeeper.sh_assign.yaml index b67683dffff..c181012384e 100644 --- a/config/crd/bases/mutations.gatekeeper.sh_assign.yaml +++ b/config/crd/bases/mutations.gatekeeper.sh_assign.yaml @@ -59,6 +59,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -106,6 +108,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -138,6 +144,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml b/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml index e737a2d9c75..bf0008fe2da 100644 --- a/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml +++ b/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml @@ -39,6 +39,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -86,6 +88,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -118,6 +124,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/config/crd/bases/mutations.gatekeeper.sh_modifyset.yaml b/config/crd/bases/mutations.gatekeeper.sh_modifyset.yaml index 5f7313929c1..4c3de3e347d 100644 --- a/config/crd/bases/mutations.gatekeeper.sh_modifyset.yaml +++ b/config/crd/bases/mutations.gatekeeper.sh_modifyset.yaml @@ -59,6 +59,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -106,6 +108,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -138,6 +144,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/manifest_staging/charts/gatekeeper/crds/assign-customresourcedefinition.yaml b/manifest_staging/charts/gatekeeper/crds/assign-customresourcedefinition.yaml index 43be352a6e3..76cfa809cff 100644 --- a/manifest_staging/charts/gatekeeper/crds/assign-customresourcedefinition.yaml +++ b/manifest_staging/charts/gatekeeper/crds/assign-customresourcedefinition.yaml @@ -63,6 +63,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -110,6 +112,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -142,6 +148,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml b/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml index 75df3ed1bcc..7110f189983 100644 --- a/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml +++ b/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml @@ -43,6 +43,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -90,6 +92,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -122,6 +128,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/manifest_staging/charts/gatekeeper/crds/modifyset-customresourcedefinition.yaml b/manifest_staging/charts/gatekeeper/crds/modifyset-customresourcedefinition.yaml index 813f8ef77b1..b4e5b6b02a6 100644 --- a/manifest_staging/charts/gatekeeper/crds/modifyset-customresourcedefinition.yaml +++ b/manifest_staging/charts/gatekeeper/crds/modifyset-customresourcedefinition.yaml @@ -63,6 +63,8 @@ spec: properties: excludedNamespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array kinds: @@ -110,6 +112,10 @@ spec: description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object + name: + description: 'Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string namespaceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. properties: @@ -142,6 +148,8 @@ spec: type: object namespaces: items: + description: 'A string that supports globbing at its end. Ex: "kube-*" will match "kube-system" or "kube-public". The asterisk is required for wildcard matching.' + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ type: string type: array scope: diff --git a/pkg/mutation/match/match.go b/pkg/mutation/match/match.go index 137a972eb7e..1a7c97dc347 100644 --- a/pkg/mutation/match/match.go +++ b/pkg/mutation/match/match.go @@ -2,8 +2,8 @@ package match import ( "errors" - "strings" + "github.com/open-policy-agent/gatekeeper/pkg/util" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -46,10 +46,14 @@ func (a ApplyTo) Flatten() []schema.GroupVersionKind { type Match struct { Kinds []Kinds `json:"kinds,omitempty"` Scope apiextensionsv1.ResourceScope `json:"scope,omitempty"` - Namespaces []string `json:"namespaces,omitempty"` - ExcludedNamespaces []string `json:"excludedNamespaces,omitempty"` + Namespaces []util.PrefixWildcard `json:"namespaces,omitempty"` + ExcludedNamespaces []util.PrefixWildcard `json:"excludedNamespaces,omitempty"` LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + // Name is the name of an object. If defined, it will match against objects with the specified + // name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both + // `pod-a` and `pod-b`. + Name util.PrefixWildcard `json:"name,omitempty"` } // Kinds accepts a list of objects with apiGroups and kinds fields @@ -79,6 +83,7 @@ func Matches(match *Match, obj client.Object, ns *corev1.Namespace) (bool, error excludedNamespacesMatch, labelSelectorMatch, namespaceSelectorMatch, + namesMatch, } for _, fn := range topLevelMatchers { @@ -142,7 +147,7 @@ func excludedNamespacesMatch(match *Match, obj client.Object, ns *corev1.Namespa } for _, n := range match.ExcludedNamespaces { - if ns.Name == n || prefixMatch(n, ns.Name) { + if n.Matches(ns.Name) { return false, nil } } @@ -157,7 +162,7 @@ func namespacesMatch(match *Match, obj client.Object, ns *corev1.Namespace) (boo } for _, n := range match.Namespaces { - if ns.Name == n || prefixMatch(n, ns.Name) { + if n.Matches(ns.Name) { return true, nil } } @@ -206,6 +211,17 @@ func kindsMatch(match *Match, obj client.Object, ns *corev1.Namespace) (bool, er return false, nil } +func namesMatch(match *Match, obj client.Object, ns *corev1.Namespace) (bool, error) { + // A blank string could be undefined or an intentional blank string by the user. Either way, + // we will assume this means "any name". This goes with the undefined == match everything + // pattern that we've already got going in the Match. + if match.Name == "" { + return true, nil + } + + return match.Name.Matches(obj.GetName()), nil +} + func scopeMatch(match *Match, obj client.Object, ns *corev1.Namespace) (bool, error) { clusterScoped := ns == nil || isNamespace(obj) @@ -222,17 +238,6 @@ func scopeMatch(match *Match, obj client.Object, ns *corev1.Namespace) (bool, er return true, nil } -// prefixMatch matches checks if the candidate contains the prefix defined in the source. -// The source is expected to end with a "*", which acts as a glob. It is removed when -// performing the prefix-based match. -func prefixMatch(source, candidate string) bool { - if !strings.HasSuffix(source, "*") { - return false - } - - return strings.HasPrefix(candidate, strings.TrimSuffix(source, "*")) -} - // AppliesTo checks if any item the given slice of ApplyTo applies to the given object. func AppliesTo(applyTo []ApplyTo, obj runtime.Object) bool { gvk := obj.GetObjectKind().GroupVersionKind() diff --git a/pkg/mutation/match/match_test.go b/pkg/mutation/match/match_test.go index c31f33c061f..a673992c03b 100644 --- a/pkg/mutation/match/match_test.go +++ b/pkg/mutation/match/match_test.go @@ -5,6 +5,7 @@ import ( "testing" configv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/config/v1alpha1" + "github.com/open-policy-agent/gatekeeper/pkg/util" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -166,7 +167,7 @@ func TestMatch(t *testing.T) { tname: "namespace matches", toMatch: makeObject("kind", "group", "namespace", "name"), match: Match{ - Namespaces: []string{"nonmatching", "namespace"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "namespace"}, }, namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "namespace"}}, shouldMatch: true, @@ -176,7 +177,7 @@ func TestMatch(t *testing.T) { tname: "namespaces configured, but cluster scoped", toMatch: makeObject("kind", "group", "", "name"), match: Match{ - Namespaces: []string{"nonmatching", "namespace"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "namespace"}, }, namespace: nil, shouldMatch: true, @@ -185,7 +186,7 @@ func TestMatch(t *testing.T) { tname: "namespace prefix matches", toMatch: makeObject("kind", "group", "kube-system", "name"), match: Match{ - Namespaces: []string{"nonmatching", "kube-*"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "kube-*"}, }, namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}}, shouldMatch: true, @@ -194,7 +195,7 @@ func TestMatch(t *testing.T) { tname: "namespace is not in the matches list", toMatch: makeObject("kind", "group", "namespace", "name"), match: Match{ - Namespaces: []string{"nonmatching", "notmatchingeither"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "notmatchingeither"}, }, namespace: &corev1.Namespace{}, shouldMatch: false, @@ -203,7 +204,7 @@ func TestMatch(t *testing.T) { tname: "namespace fails if clusterscoped", toMatch: makeObject("kind", "group", "namespace", "name"), match: Match{ - Namespaces: []string{"nonmatching", "namespace"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "namespace"}, Scope: apiextensionsv1.ClusterScoped, }, namespace: &corev1.Namespace{}, @@ -219,8 +220,8 @@ func TestMatch(t *testing.T) { APIGroups: []string{"group"}, }, }, - Namespaces: []string{"nonmatching", "namespace"}, - ExcludedNamespaces: []string{"namespace"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "namespace"}, + ExcludedNamespaces: []util.PrefixWildcard{"namespace"}, }, namespace: &corev1.Namespace{}, shouldMatch: false, @@ -236,8 +237,8 @@ func TestMatch(t *testing.T) { APIGroups: []string{"group"}, }, }, - Namespaces: []string{"nonmatching", "namespace"}, - ExcludedNamespaces: []string{"namespace"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "namespace"}, + ExcludedNamespaces: []util.PrefixWildcard{"namespace"}, }, namespace: nil, shouldMatch: true, @@ -252,8 +253,8 @@ func TestMatch(t *testing.T) { APIGroups: []string{"group"}, }, }, - Namespaces: []string{"nonmatching", "kube-*"}, - ExcludedNamespaces: []string{"kube-*"}, + Namespaces: []util.PrefixWildcard{"nonmatching", "kube-*"}, + ExcludedNamespaces: []util.PrefixWildcard{"kube-*"}, }, namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}}, shouldMatch: false, @@ -431,6 +432,52 @@ func TestMatch(t *testing.T) { }, shouldMatch: false, }, + { + tname: "match name", + toMatch: makeObject("kind", "group", "namespace", "name-foo"), + match: Match{ + Name: "name-foo", + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "match wildcard name", + toMatch: makeObject("kind", "group", "namespace", "name-foo"), + match: Match{ + Name: "name-*", + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "missing asterisk in name wildcard does not match", + toMatch: makeObject("kind", "group", "namespace", "name-foo"), + match: Match{ + Name: "name-", + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "wrong name does not match", + toMatch: makeObject("kind", "group", "namespace", "name-foo"), + match: Match{ + Name: "name-bar", + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "no match with correct name and wrong namespace", + toMatch: makeObject("kind", "group", "namespace", "name-foo"), + match: Match{ + Name: "name-foo", + Namespaces: []util.PrefixWildcard{"other-namespace"}, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, } for _, tc := range table { t.Run(tc.tname, func(t *testing.T) { diff --git a/pkg/mutation/match/zz_generated.deepcopy.go b/pkg/mutation/match/zz_generated.deepcopy.go index fba096d3a98..441454ed93e 100644 --- a/pkg/mutation/match/zz_generated.deepcopy.go +++ b/pkg/mutation/match/zz_generated.deepcopy.go @@ -20,6 +20,7 @@ limitations under the License. package match import ( + "github.com/open-policy-agent/gatekeeper/pkg/util" "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -90,12 +91,12 @@ func (in *Match) DeepCopyInto(out *Match) { } if in.Namespaces != nil { in, out := &in.Namespaces, &out.Namespaces - *out = make([]string, len(*in)) + *out = make([]util.PrefixWildcard, len(*in)) copy(*out, *in) } if in.ExcludedNamespaces != nil { in, out := &in.ExcludedNamespaces, &out.ExcludedNamespaces - *out = make([]string, len(*in)) + *out = make([]util.PrefixWildcard, len(*in)) copy(*out, *in) } if in.LabelSelector != nil { diff --git a/pkg/mutation/mutators/conversion_test.go b/pkg/mutation/mutators/conversion_test.go index cd7f7fa9a22..f591a767454 100644 --- a/pkg/mutation/mutators/conversion_test.go +++ b/pkg/mutation/mutators/conversion_test.go @@ -8,6 +8,7 @@ import ( mutationsv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" "github.com/open-policy-agent/gatekeeper/pkg/mutation/match" "github.com/open-policy-agent/gatekeeper/pkg/mutation/path/tester" + "github.com/open-policy-agent/gatekeeper/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -236,7 +237,7 @@ func TestAssignMetadataHasDiff(t *testing.T) { { "differentMatch", func(a *mutationsv1alpha1.AssignMetadata) { - a.Spec.Match.Namespaces = []string{"foo", "bar"} + a.Spec.Match.Namespaces = []util.PrefixWildcard{"foo", "bar"} }, true, }, diff --git a/pkg/target/regolib/name_selector_test.rego b/pkg/target/regolib/name_selector_test.rego new file mode 100644 index 00000000000..deb32b13c80 --- /dev/null +++ b/pkg/target/regolib/name_selector_test.rego @@ -0,0 +1,26 @@ +package target + +test_name_match { + matches_name({"name": "foo"}) + with input.review.name as "foo" +} + +test_name_no_match { + not matches_name({"name": "bar"}) + with input.review.name as "foo" +} + +test_no_name_is_match { + matches_name({}) + with input.review.name as "foo" +} + +test_wildcard_name_match { + matches_name({"name": "foo*"}) + with input.review.name as "foobar" +} + +test_wildcard_no_asterisk_no_match { + not matches_name({"name": "foo"}) + with input.review.name as "foobar" +} diff --git a/pkg/target/regolib/namespace_selector_test.rego b/pkg/target/regolib/namespace_selector_test.rego index 4ff55269f77..df561f0c0c3 100644 --- a/pkg/target/regolib/namespace_selector_test.rego +++ b/pkg/target/regolib/namespace_selector_test.rego @@ -1,24 +1,24 @@ package target -test_name_match { +test_namespace_match { matches_namespaces({"namespaces": ["kube-system", "gatekeeper-system"]}) with input.review.kind as pod_kind with input.review.namespace as "gatekeeper-system" } -test_name_no_match { +test_namespace_no_match { not matches_namespaces({"namespaces": ["kube-system", "gatekeeper-system"]}) with input.review.kind as pod_kind with input.review.namespace as "burrito" } -test_name_match_is_ns { +test_namespace_match_is_ns { matches_namespaces({"namespaces": ["kube-system", "gatekeeper-system"]}) with input.review.kind as ns_kind with input.review.object.metadata.name as "gatekeeper-system" } -test_name_no_match_is_ns { +test_namespace_no_match_is_ns { not matches_namespaces({"namespaces": ["kube-system", "gatekeeper-system"]}) with input.review.kind as ns_kind with input.review.object.metadata.name as "front-end" diff --git a/pkg/target/regolib/src.rego b/pkg/target/regolib/src.rego index a3168d70b0a..9a96f72b82b 100644 --- a/pkg/target/regolib/src.rego +++ b/pkg/target/regolib/src.rego @@ -34,6 +34,8 @@ matching_constraints[constraint] { matches_scope(match) + matches_name(match) + label_selector := get_default(match, "labelSelector", {}) any_labelselector_match(label_selector) } @@ -152,6 +154,51 @@ kind_matches(ks) { ks.kinds[_] == input.review.kind.kind } +####################### +# Name Selector Logic # +####################### + +matches_name(match) { + not has_field(match, "name") +} + +matches_name(match) { + has_field(match, "name") + input.review.name == match.name +} + +matches_name(match) { + has_field(match, "name") + input.review.object.metadata.name == match.name +} + +# oldObject covers Updates and Deletes. The name of an object can't be changed, so there's no +# need to worry about a difference between object and oldObject. +matches_name(match) { + has_field(match, "name") + input.review.oldObject.metadata.name == match.name +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.name) +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.object.metadata.name) +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.oldObject.metadata.name) +} + +wildcard_name_match(wildcard, subject) { + endswith(wildcard, "*") + glob.match(wildcard, [], subject) +} + ######################## # Scope Selector Logic # ######################## @@ -337,17 +384,17 @@ matches_namespaces(match) { has_field(match, "namespaces") not always_match_ns_selectors get_ns_name[ns] - wild_nss := wildcard_namespaces(match.namespaces) + wild_nss := wildcard_tokens(match.namespaces) prefix_glob_match(wild_nss, ns) } -wildcard_namespaces(ns_array) = out { - out := [ nss | endswith(ns_array[i], "*") - nss := ns_array[i] ] +wildcard_tokens(token_array) = out { + out := [ wld_tokens | endswith(token_array[i], "*") + wld_tokens := token_array[i] ] } -prefix_glob_match(match_nss, object_ns) { - glob.match(match_nss[_], [], object_ns) +prefix_glob_match(matchables, subject) { + glob.match(matchables[_], [], subject) } does_not_match_excludednamespaces(match) { @@ -369,7 +416,7 @@ does_not_match_excludednamespaces(match) { not nss[ns] # Check for prefix matches - wild_ex_nss := wildcard_namespaces(match.excludedNamespaces) + wild_ex_nss := wildcard_tokens(match.excludedNamespaces) not prefix_glob_match(wild_ex_nss, ns) } diff --git a/pkg/target/target.go b/pkg/target/target.go index 2ef78f51811..78256021c9c 100644 --- a/pkg/target/target.go +++ b/pkg/target/target.go @@ -322,6 +322,7 @@ func (h *K8sValidationTarget) MatchSchema() apiextensions.JSONSchemaProps { }, } + // Make sure to copy description changes into pkg/mutation/match/match.go's `Match` struct. return apiextensions.JSONSchemaProps{ Type: "object", Properties: map[string]apiextensions.JSONSchemaProps{ @@ -349,6 +350,11 @@ func (h *K8sValidationTarget) MatchSchema() apiextensions.JSONSchemaProps { "Namespaced", }, }, + "name": { + Type: "string", + Description: "Name is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`.", + Pattern: wildcardNSPattern, + }, }, } } diff --git a/pkg/target/target_integration_test.go b/pkg/target/target_integration_test.go index 90d27d07f9f..1bb030c7197 100644 --- a/pkg/target/target_integration_test.go +++ b/pkg/target/target_integration_test.go @@ -39,6 +39,7 @@ spec: } ` ) +const testResourceName = "test-resource" type buildArg func(*unstructured.Unstructured) @@ -102,6 +103,14 @@ func setScope(scope string) buildArg { } } +func setName(name string) buildArg { + return func(obj *unstructured.Unstructured) { + if err := unstructured.SetNestedField(obj.Object, name, "spec", "match", "name"); err != nil { + panic(err) + } + } +} + func makeConstraint(o ...buildArg) *unstructured.Unstructured { u := &unstructured.Unstructured{} u.SetName("my-constraint") @@ -114,6 +123,7 @@ func makeConstraint(o ...buildArg) *unstructured.Unstructured { func makeResource(group, kind string, labels ...map[string]string) *unstructured.Unstructured { u := &unstructured.Unstructured{} + u.SetName(testResourceName) u.SetGroupVersionKind(schema.GroupVersionKind{Group: group, Version: "v1", Kind: kind}) if len(labels) > 0 { u.SetLabels(labels[0]) @@ -215,6 +225,27 @@ func TestConstraintEnforcement(t *testing.T) { constraint: makeConstraint(setKinds([]string{"different"}, []string{"Thing"})), allowed: true, }, + { + name: "match name", + obj: makeResource("some", "Thing"), + ns: makeNamespace("my-ns"), + constraint: makeConstraint(setName(testResourceName)), + allowed: false, + }, + { + name: "no match name", + obj: makeResource("some", "Thing"), + ns: makeNamespace("my-ns"), + constraint: makeConstraint(setName("other-name")), + allowed: true, + }, + { + name: "match name wildcard", + obj: makeResource("some", "Thing"), + ns: makeNamespace("my-ns"), + constraint: makeConstraint(setName("test-*")), + allowed: false, + }, { name: "match everything", obj: makeResource("some", "Thing", map[string]string{"obj": "label"}), diff --git a/pkg/target/target_template_source.go b/pkg/target/target_template_source.go index f46b722b22e..611822a415a 100644 --- a/pkg/target/target_template_source.go +++ b/pkg/target/target_template_source.go @@ -39,6 +39,8 @@ matching_constraints[constraint] { matches_scope(match) + matches_name(match) + label_selector := get_default(match, "labelSelector", {}) any_labelselector_match(label_selector) } @@ -157,6 +159,51 @@ kind_matches(ks) { ks.kinds[_] == input.review.kind.kind } +####################### +# Name Selector Logic # +####################### + +matches_name(match) { + not has_field(match, "name") +} + +matches_name(match) { + has_field(match, "name") + input.review.name == match.name +} + +matches_name(match) { + has_field(match, "name") + input.review.object.metadata.name == match.name +} + +# oldObject covers Updates and Deletes. The name of an object can't be changed, so there's no +# need to worry about a difference between object and oldObject. +matches_name(match) { + has_field(match, "name") + input.review.oldObject.metadata.name == match.name +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.name) +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.object.metadata.name) +} + +matches_name(match) { + has_field(match, "name") + wildcard_name_match(match.name, input.review.oldObject.metadata.name) +} + +wildcard_name_match(wildcard, subject) { + endswith(wildcard, "*") + glob.match(wildcard, [], subject) +} + ######################## # Scope Selector Logic # ######################## @@ -342,17 +389,17 @@ matches_namespaces(match) { has_field(match, "namespaces") not always_match_ns_selectors get_ns_name[ns] - wild_nss := wildcard_namespaces(match.namespaces) + wild_nss := wildcard_tokens(match.namespaces) prefix_glob_match(wild_nss, ns) } -wildcard_namespaces(ns_array) = out { - out := [ nss | endswith(ns_array[i], "*") - nss := ns_array[i] ] +wildcard_tokens(token_array) = out { + out := [ wld_tokens | endswith(token_array[i], "*") + wld_tokens := token_array[i] ] } -prefix_glob_match(match_nss, object_ns) { - glob.match(match_nss[_], [], object_ns) +prefix_glob_match(matchables, subject) { + glob.match(matchables[_], [], subject) } does_not_match_excludednamespaces(match) { @@ -374,7 +421,7 @@ does_not_match_excludednamespaces(match) { not nss[ns] # Check for prefix matches - wild_ex_nss := wildcard_namespaces(match.excludedNamespaces) + wild_ex_nss := wildcard_tokens(match.excludedNamespaces) not prefix_glob_match(wild_ex_nss, ns) } diff --git a/website/docs/howto.md b/website/docs/howto.md index fc5d99320bc..3e3533a1bd4 100644 --- a/website/docs/howto.md +++ b/website/docs/howto.md @@ -76,9 +76,10 @@ Note the `match` field, which defines the scope of objects to which a given cons * `kinds` accepts a list of objects with `apiGroups` and `kinds` fields that list the groups/kinds of objects to which the constraint will apply. If multiple groups/kinds objects are specified, only one match is needed for the resource to be in scope. * `scope` accepts `*`, `Cluster`, or `Namespaced` which determines if cluster-scoped and/or namesapced-scoped resources are selected. (defaults to `*`) - * `namespaces` is a list of namespace names. If defined, a constraint will only apply to resources in a listed namespace. - * `excludedNamespaces` is a list of namespace names. If defined, a constraint will only apply to resources not in a listed namespace. + * `namespaces` is a list of namespace names. If defined, a constraint will only apply to resources in a listed namespace. Namespaces also supports a prefix-based glob. For example, `namespaces: [kube-*]` would match both `kube-system` and `kube-public`. + * `excludedNamespaces` is a list of namespace names. If defined, a constraint will only apply to resources not in a listed namespace. ExcludedNamespaces also supports a prefix-based glob. For example, `excludedNamespaces: [kube-*]` would match both `kube-system` and `kube-public`. * `labelSelector` is a standard Kubernetes label selector. * `namespaceSelector` is a standard Kubernetes namespace selector. If defined, make sure to add `Namespaces` to your `configs.config.gatekeeper.sh` object to ensure namespaces are synced into OPA. Refer to the [Replicating Data section](sync.md) for more details. + * `name` is the name of an object. If defined, it will match against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` would match both `pod-a` and `pod-b`. Note that if multiple matchers are specified, a resource must satisfy each top-level matcher (`kinds`, `namespaces`, etc.) to be in scope. Each top-level matcher has its own semantics for what qualifies as a match. An empty matcher is deemed to be inclusive (matches everything). Also understand `namespaces`, `excludedNamespaces`, and `namespaceSelector` will match on cluster scoped resources which are not namespaced. To avoid this adjust the `scope` to `Namespaced`.