From caf342f29e0786ecfc973f7d80905316342010b8 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Mon, 26 Aug 2024 15:16:59 -0700 Subject: [PATCH] fix(k8srequiredlabels): CEL broke when allowedRegex was empty (#583) The CEL implementation of this policy always expected the `allowedRegex` value to be set for a given `key` value in the contraint parameters. Existing users of the template could (via rego) require only that a given label key exist, but assert nothing about the value of that label. This change makes the CEL implementation uphold that same contract. Signed-off-by: juliankatz --- .../requiredlabels/1.1.2/artifacthub-pkg.yml | 22 +++++ .../requiredlabels/1.1.2/kustomization.yaml | 2 + .../all-must-have-owner/constraint.yaml | 14 +++ .../all-must-have-owner/example_allowed.yaml | 6 ++ .../example_disallowed.yaml | 4 + .../example_disallowed_label_value.yaml | 6 ++ .../verify-label-key-only/constraint.yaml | 13 +++ .../example_allowed.yaml | 10 +++ .../example_disallowed.yaml | 10 +++ .../general/requiredlabels/1.1.2/suite.yaml | 33 +++++++ .../requiredlabels/1.1.2/template.yaml | 79 +++++++++++++++++ .../verify-label-key-only/constraint.yaml | 13 +++ .../example_allowed.yaml | 10 +++ .../example_disallowed.yaml | 10 +++ library/general/requiredlabels/suite.yaml | 12 +++ library/general/requiredlabels/template.yaml | 4 +- src/general/requiredlabels/constraint.tmpl | 2 +- src/general/requiredlabels/src.cel | 4 +- website/docs/validation/requiredlabels.md | 85 ++++++++++++++++++- 19 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/artifacthub-pkg.yml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/kustomization.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/constraint.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_allowed.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed_label_value.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/constraint.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_allowed.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_disallowed.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/suite.yaml create mode 100644 artifacthub/library/general/requiredlabels/1.1.2/template.yaml create mode 100644 library/general/requiredlabels/samples/verify-label-key-only/constraint.yaml create mode 100644 library/general/requiredlabels/samples/verify-label-key-only/example_allowed.yaml create mode 100644 library/general/requiredlabels/samples/verify-label-key-only/example_disallowed.yaml diff --git a/artifacthub/library/general/requiredlabels/1.1.2/artifacthub-pkg.yml b/artifacthub/library/general/requiredlabels/1.1.2/artifacthub-pkg.yml new file mode 100644 index 000000000..eb0cd9195 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.2 +name: k8srequiredlabels +displayName: Required Labels +createdAt: "2024-08-26T21:23:58Z" +description: Requires resources to contain specified labels, with values matching provided regular expressions. +digest: 707994cb63de5a067bc9808caa5f66eb7831a4a9a19849c7eb2bb24b112e0726 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/requiredlabels +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Required Labels + Requires resources to contain specified labels, with values matching provided regular expressions. +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/general/requiredlabels/1.1.2/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/general/requiredlabels/1.1.2/kustomization.yaml b/artifacthub/library/general/requiredlabels/1.1.2/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/constraint.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/constraint.yaml new file mode 100644 index 000000000..806e9862f --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredLabels +metadata: + name: all-must-have-owner +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Namespace"] + parameters: + message: "All namespaces must have an `owner` label that points to your company username" + labels: + - key: owner + allowedRegex: "^[a-zA-Z]+.agilebank.demo$" diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_allowed.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_allowed.yaml new file mode 100644 index 000000000..e2d3b9c03 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_allowed.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: allowed-namespace + labels: + owner: user.agilebank.demo diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed.yaml new file mode 100644 index 000000000..a7a53610a --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: disallowed-namespace diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed_label_value.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed_label_value.yaml new file mode 100644 index 000000000..36ae77176 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/all-must-have-owner/example_disallowed_label_value.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: disallowed-namespace + labels: + owner: user diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/constraint.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/constraint.yaml new file mode 100644 index 000000000..ead40234a --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredLabels +metadata: + name: must-have-pizza +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + message: "All pods must have label of key `pizza` regardless of the label's value" + labels: + - key: pizza diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_allowed.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_allowed.yaml new file mode 100644 index 000000000..3a904e333 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_allowed.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: has-pizza + labels: + pizza: is-great +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_disallowed.yaml b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_disallowed.yaml new file mode 100644 index 000000000..fcf5b3e94 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/samples/verify-label-key-only/example_disallowed.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: does-not-have-pizza + labels: + taco: is-great +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/general/requiredlabels/1.1.2/suite.yaml b/artifacthub/library/general/requiredlabels/1.1.2/suite.yaml new file mode 100644 index 000000000..9a0d2d757 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/suite.yaml @@ -0,0 +1,33 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: requiredlabels +tests: +- name: must-have-owner + template: template.yaml + constraint: samples/all-must-have-owner/constraint.yaml + cases: + - name: example-allowed + object: samples/all-must-have-owner/example_allowed.yaml + assertions: + - violations: no + - name: example-disallowed + object: samples/all-must-have-owner/example_disallowed.yaml + assertions: + - violations: yes + - name: example-disallowed-label-value + object: samples/all-must-have-owner/example_disallowed_label_value.yaml + assertions: + - violations: yes +- name: must-have-key + template: template.yaml + constraint: samples/verify-label-key-only/constraint.yaml + cases: + - name: label-present + object: samples/verify-label-key-only/example_allowed.yaml + assertions: + - violations: no + - name: label-missing + object: samples/verify-label-key-only/example_disallowed.yaml + assertions: + - violations: yes diff --git a/artifacthub/library/general/requiredlabels/1.1.2/template.yaml b/artifacthub/library/general/requiredlabels/1.1.2/template.yaml new file mode 100644 index 000000000..5404fadc7 --- /dev/null +++ b/artifacthub/library/general/requiredlabels/1.1.2/template.yaml @@ -0,0 +1,79 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8srequiredlabels + annotations: + metadata.gatekeeper.sh/title: "Required Labels" + metadata.gatekeeper.sh/version: 1.1.2 + description: >- + Requires resources to contain specified labels, with values matching + provided regular expressions. +spec: + crd: + spec: + names: + kind: K8sRequiredLabels + validation: + openAPIV3Schema: + type: object + properties: + message: + type: string + labels: + type: array + description: >- + A list of labels and values the object must specify. + items: + type: object + properties: + key: + type: string + description: >- + The required label. + allowedRegex: + type: string + description: >- + If specified, a regular expression the annotation's value + must match. The value must contain at least one match for + the regular expression. + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + validations: + - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels))' + messageExpression: '"missing required label, requires all of: " + variables.params.labels.map(entry, entry.key).join(", ")' + - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && (!has(entry.allowedRegex) || string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex)))))' + message: "regex mismatch" + - engine: Rego + source: + rego: | + package k8srequiredlabels + + get_message(parameters, _default) := _default { + not parameters.message + } + + get_message(parameters, _) := parameters.message + + violation[{"msg": msg, "details": {"missing_labels": missing}}] { + provided := {label | input.review.object.metadata.labels[label]} + required := {label | label := input.parameters.labels[_].key} + missing := required - provided + count(missing) > 0 + def_msg := sprintf("you must provide labels: %v", [missing]) + msg := get_message(input.parameters, def_msg) + } + + violation[{"msg": msg}] { + value := input.review.object.metadata.labels[key] + expected := input.parameters.labels[_] + expected.key == key + # do not match if allowedRegex is not defined, or is an empty string + expected.allowedRegex != "" + not regex.match(expected.allowedRegex, value) + def_msg := sprintf("Label <%v: %v> does not satisfy allowed regex: %v", [key, value, expected.allowedRegex]) + msg := get_message(input.parameters, def_msg) + } + diff --git a/library/general/requiredlabels/samples/verify-label-key-only/constraint.yaml b/library/general/requiredlabels/samples/verify-label-key-only/constraint.yaml new file mode 100644 index 000000000..ead40234a --- /dev/null +++ b/library/general/requiredlabels/samples/verify-label-key-only/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredLabels +metadata: + name: must-have-pizza +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + message: "All pods must have label of key `pizza` regardless of the label's value" + labels: + - key: pizza diff --git a/library/general/requiredlabels/samples/verify-label-key-only/example_allowed.yaml b/library/general/requiredlabels/samples/verify-label-key-only/example_allowed.yaml new file mode 100644 index 000000000..3a904e333 --- /dev/null +++ b/library/general/requiredlabels/samples/verify-label-key-only/example_allowed.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: has-pizza + labels: + pizza: is-great +spec: + containers: + - name: nginx + image: nginx diff --git a/library/general/requiredlabels/samples/verify-label-key-only/example_disallowed.yaml b/library/general/requiredlabels/samples/verify-label-key-only/example_disallowed.yaml new file mode 100644 index 000000000..fcf5b3e94 --- /dev/null +++ b/library/general/requiredlabels/samples/verify-label-key-only/example_disallowed.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: does-not-have-pizza + labels: + taco: is-great +spec: + containers: + - name: nginx + image: nginx diff --git a/library/general/requiredlabels/suite.yaml b/library/general/requiredlabels/suite.yaml index 0995d8ea0..9a0d2d757 100644 --- a/library/general/requiredlabels/suite.yaml +++ b/library/general/requiredlabels/suite.yaml @@ -19,3 +19,15 @@ tests: object: samples/all-must-have-owner/example_disallowed_label_value.yaml assertions: - violations: yes +- name: must-have-key + template: template.yaml + constraint: samples/verify-label-key-only/constraint.yaml + cases: + - name: label-present + object: samples/verify-label-key-only/example_allowed.yaml + assertions: + - violations: no + - name: label-missing + object: samples/verify-label-key-only/example_disallowed.yaml + assertions: + - violations: yes diff --git a/library/general/requiredlabels/template.yaml b/library/general/requiredlabels/template.yaml index 7a47afe27..5404fadc7 100644 --- a/library/general/requiredlabels/template.yaml +++ b/library/general/requiredlabels/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8srequiredlabels annotations: metadata.gatekeeper.sh/title: "Required Labels" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Requires resources to contain specified labels, with values matching provided regular expressions. @@ -44,7 +44,7 @@ spec: validations: - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels))' messageExpression: '"missing required label, requires all of: " + variables.params.labels.map(entry, entry.key).join(", ")' - - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex))))' + - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && (!has(entry.allowedRegex) || string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex)))))' message: "regex mismatch" - engine: Rego source: diff --git a/src/general/requiredlabels/constraint.tmpl b/src/general/requiredlabels/constraint.tmpl index d3a57e4fb..fd436f50b 100644 --- a/src/general/requiredlabels/constraint.tmpl +++ b/src/general/requiredlabels/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8srequiredlabels annotations: metadata.gatekeeper.sh/title: "Required Labels" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Requires resources to contain specified labels, with values matching provided regular expressions. diff --git a/src/general/requiredlabels/src.cel b/src/general/requiredlabels/src.cel index 2637d64b0..41ae481dd 100644 --- a/src/general/requiredlabels/src.cel +++ b/src/general/requiredlabels/src.cel @@ -1,5 +1,5 @@ validations: - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels))' messageExpression: '"missing required label, requires all of: " + variables.params.labels.map(entry, entry.key).join(", ")' -- expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex))))' - message: "regex mismatch" \ No newline at end of file +- expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && (!has(entry.allowedRegex) || string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex)))))' + message: "regex mismatch" diff --git a/website/docs/validation/requiredlabels.md b/website/docs/validation/requiredlabels.md index 3508ac578..2bc76c52a 100644 --- a/website/docs/validation/requiredlabels.md +++ b/website/docs/validation/requiredlabels.md @@ -16,7 +16,7 @@ metadata: name: k8srequiredlabels annotations: metadata.gatekeeper.sh/title: "Required Labels" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Requires resources to contain specified labels, with values matching provided regular expressions. @@ -56,7 +56,7 @@ spec: validations: - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels))' messageExpression: '"missing required label, requires all of: " + variables.params.labels.map(entry, entry.key).join(", ")' - - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex))))' + - expression: '(has(variables.anyObject.metadata) && variables.params.labels.all(entry, has(variables.anyObject.metadata.labels) && entry.key in variables.anyObject.metadata.labels && (!has(entry.allowedRegex) || string(variables.anyObject.metadata.labels[entry.key]).matches(string(entry.allowedRegex)))))' message: "regex mismatch" - engine: Rego source: @@ -189,4 +189,85 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper- +
+must-have-key + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredLabels +metadata: + name: must-have-pizza +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + message: "All pods must have label of key `pizza` regardless of the label's value" + labels: + - key: pizza + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredlabels/samples/verify-label-key-only/constraint.yaml +``` + +
+ +
+label-present + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: has-pizza + labels: + pizza: is-great +spec: + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredlabels/samples/verify-label-key-only/example_allowed.yaml +``` + +
+
+label-missing + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: does-not-have-pizza + labels: + taco: is-great +spec: + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredlabels/samples/verify-label-key-only/example_disallowed.yaml +``` + +
+ +
\ No newline at end of file