diff --git a/src/fixtures/data/admission-create-clusterrole.json b/src/fixtures/data/admission-create-clusterrole.json new file mode 100644 index 000000000..cc6327bd5 --- /dev/null +++ b/src/fixtures/data/admission-create-clusterrole.json @@ -0,0 +1,52 @@ +{ + "uid": "2ac28f03-c045-4af6-86f1-aa0007571863", + "kind": { + "group": "rbac.authorization.k8s.io", + "version": "v1", + "kind": "ClusterRole" + }, + "resource": { + "group": "rbac.authorization.k8s.io", + "version": "v1", + "resource": "clusterroles" + }, + "requestKind": { + "group": "rbac.authorization.k8s.io", + "version": "v1", + "kind": "ClusterRole" + }, + "requestResource": { + "group": "rbac.authorization.k8s.io", + "version": "v1", + "resource": "clusterroles" + }, + "name": "pod-creator", + "operation": "CREATE", + "userInfo": { + "username": "system:admin", + "groups": ["system:masters", "system:authenticated"] + }, + "object": { + "kind": "ClusterRole", + "apiVersion": "rbac.authorization.k8s.io/v1", + "metadata": { + "name": "pod-creator", + "creationTimestamp": null + }, + "rules": [ + { + "verbs": ["create", "update", "patch"], + "apiGroups": [""], + "resources": ["pods"] + } + ] + }, + "oldObject": null, + "dryRun": false, + "options": { + "kind": "CreateOptions", + "apiVersion": "meta.k8s.io/v1", + "fieldManager": "kubectl-create", + "fieldValidation": "Strict" + } +} diff --git a/src/fixtures/data/admission-create-deployment.json b/src/fixtures/data/admission-create-deployment.json new file mode 100644 index 000000000..f3df7f7c3 --- /dev/null +++ b/src/fixtures/data/admission-create-deployment.json @@ -0,0 +1,93 @@ +{ + "uid": "501f5447-a028-4a3f-b4ac-fc56f3f78ffc", + "kind": { + "group": "apps", + "version": "v1", + "kind": "Deployment" + }, + "resource": { + "group": "apps", + "version": "v1", + "resource": "deployments" + }, + "requestKind": { + "group": "apps", + "version": "v1", + "kind": "Deployment" + }, + "requestResource": { + "group": "apps", + "version": "v1", + "resource": "deployments" + }, + "name": "lower", + "namespace": "pepr-demo", + "operation": "CREATE", + "userInfo": { + "username": "system:admin", + "groups": ["system:masters", "system:authenticated"] + }, + "object": { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "lower", + "namespace": "pepr-demo", + "creationTimestamp": null, + "labels": { + "app": "lower" + } + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "lower" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "lower" + } + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "Always" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + }, + "strategy": { + "type": "RollingUpdate", + "rollingUpdate": { + "maxUnavailable": "25%", + "maxSurge": "25%" + } + }, + "revisionHistoryLimit": 10, + "progressDeadlineSeconds": 600 + }, + "status": {} + }, + "oldObject": null, + "dryRun": false, + "options": { + "kind": "CreateOptions", + "apiVersion": "meta.k8s.io/v1", + "fieldManager": "kubectl-create", + "fieldValidation": "Strict" + } +} diff --git a/src/fixtures/data/create-pod.json b/src/fixtures/data/admission-create-pod.json similarity index 100% rename from src/fixtures/data/create-pod.json rename to src/fixtures/data/admission-create-pod.json diff --git a/src/fixtures/data/delete-pod.json b/src/fixtures/data/admission-delete-pod.json similarity index 100% rename from src/fixtures/data/delete-pod.json rename to src/fixtures/data/admission-delete-pod.json diff --git a/src/fixtures/loader.ts b/src/fixtures/loader.ts index eca823415..24f3ccee9 100644 --- a/src/fixtures/loader.ts +++ b/src/fixtures/loader.ts @@ -1,15 +1,25 @@ import { kind } from "kubernetes-fluent-client"; import { AdmissionRequest } from "../lib/types"; -import createPod from "./data/create-pod.json"; -import deletePod from "./data/delete-pod.json"; +import admissionRequestCreatePod from "./data/admission-create-pod.json"; +import admissionRequestDeletePod from "./data/admission-delete-pod.json"; +import admissionRequestCreateClusterRole from "./data/admission-create-clusterrole.json"; +import admissionRequestCreateDeployment from "./data/admission-create-deployment.json"; -export function CreatePod() { - return cloneObject(createPod); +export function AdmissionRequestCreateDeployment() { + return cloneObject(admissionRequestCreateDeployment); } -export function DeletePod() { - return cloneObject(deletePod); +export function AdmissionRequestCreatePod() { + return cloneObject(admissionRequestCreatePod); +} + +export function AdmissionRequestDeletePod() { + return cloneObject(admissionRequestDeletePod); +} + +export function AdmissionRequestCreateClusterRole() { + return cloneObject(admissionRequestCreateClusterRole); } function cloneObject(obj: unknown): AdmissionRequest { diff --git a/src/lib/assets/rbac.ts b/src/lib/assets/rbac.ts index 92a2a55c6..c575e2150 100644 --- a/src/lib/assets/rbac.ts +++ b/src/lib/assets/rbac.ts @@ -22,7 +22,6 @@ export function clusterRole( ): kind.ClusterRole { // Create the RBAC map from capabilities const rbacMap = createRBACMap(capabilities); - // Generate scoped rules from rbacMap const scopedRules = Object.keys(rbacMap).map(key => { let group: string; diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index c89927d6e..a34553c14 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -254,6 +254,37 @@ describe("mismatchedLabels", () => { }); }); +describe("missingCarriableNamespace", () => { + //[ capa ns's, KubernetesObject, result ] + it.each([ + [[], {}, false], + [[], { metadata: { namespace: "namespace" } }, false], + + [["namespace"], {}, true], + [["namespace"], { metadata: {} }, true], + [["namespace"], { metadata: { namespace: null } }, true], + [["namespace"], { metadata: { namespace: "" } }, true], + [["namespace"], { metadata: { namespace: "incorrect" } }, false], + [["namespace"], { metadata: { namespace: "namespace" } }, false], + + [["name", "space"], {}, true], + [["name", "space"], { metadata: {} }, true], + [["name", "space"], { metadata: { namespace: null } }, true], + [["name", "space"], { metadata: { namespace: "" } }, true], + [["name", "space"], { metadata: { namespace: "incorrect" } }, false], + [["name", "space"], { metadata: { namespace: "name" } }, false], + [["name", "space"], { metadata: { namespace: "space" } }, false], + [["ingress-controller"], { kind: "Namespace", metadata: { name: "ingress-controller" } }, false], + [["ingress-controller"], { kind: "Namespace", metadata: { name: "egress-controller" } }, true], + ])("given capabilityNamespaces %j and object %j, returns %s", (nss, obj, expected) => { + const object = obj as DeepPartial; + + const result = sut.missingCarriableNamespace(nss, object); + + expect(result).toBe(expected); + }); +}); + describe("uncarryableNamespace", () => { //[ capa ns's, KubernetesObject, result ] it.each([ diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index ee3f48dcd..5ebe17d12 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -216,6 +216,15 @@ export const uncarryableNamespace = allPass([ pipe((namespaceSelector, kubernetesObject) => namespaceSelector.includes(carriedNamespace(kubernetesObject)), not), ]); +export const missingCarriableNamespace = allPass([ + pipe(nthArg(0), length, gt(__, 0)), + pipe((namespaceSelector: string[], kubernetesObject: KubernetesObject): boolean => + kubernetesObject.kind === "Namespace" + ? !namespaceSelector.includes(kubernetesObject.metadata!.name!) + : !carriesNamespace(kubernetesObject), + ), +]); + export const carriesIgnoredNamespace = allPass([ pipe(nthArg(0), length, gt(__, 0)), pipe(nthArg(1), carriesNamespace), diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index 91b20040c..cf4ecdfd4 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -4,7 +4,7 @@ import { expect, test, describe } from "@jest/globals"; import { kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import * as fc from "fast-check"; -import { CreatePod, DeletePod } from "../../fixtures/loader"; +import { AdmissionRequestCreatePod, AdmissionRequestDeletePod } from "../../fixtures/loader"; import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event } from "../enums"; @@ -122,7 +122,7 @@ test("create: should reject when regex name does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, ); @@ -144,7 +144,7 @@ test("create: should not reject when regex name does match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -164,7 +164,7 @@ test("delete: should reject when regex name does not match", () => { }, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, ); @@ -186,7 +186,7 @@ test("delete: should not reject when regex name does match", () => { }, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -206,7 +206,7 @@ test("create: should not reject when regex namespace does match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -226,7 +226,7 @@ test("create: should reject when regex namespace does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, ); @@ -248,7 +248,7 @@ test("delete: should reject when regex namespace does not match", () => { }, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, ); @@ -270,7 +270,7 @@ test("delete: should not reject when regex namespace does match", () => { }, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -290,7 +290,7 @@ test("delete: should reject when name does not match", () => { }, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name '.*' but Object carries '.*'./, ); @@ -316,7 +316,7 @@ test("should reject when kind does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines kind '.*' but Request declares '.*'./, @@ -343,7 +343,7 @@ test("should reject when group does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines group '.*' but Request declares '.*'./, @@ -370,7 +370,7 @@ test("should reject when version does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines version '.*' but Request declares '.*'./, @@ -393,7 +393,7 @@ test("should allow when group, version, and kind match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -418,7 +418,7 @@ test("should allow when kind match and others are empty", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -439,7 +439,7 @@ test("should reject when the capability namespace does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( /Ignoring Admission Callback: Object carries namespace '.*' but namespaces allowed by Capability are '.*'./, @@ -462,7 +462,7 @@ test("should reject when namespace does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespaces '.*' but Object carries '.*'./, @@ -485,7 +485,7 @@ test("should allow when namespace is match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -508,7 +508,7 @@ test("should reject when label does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines labels '.*' but Object carries '.*'./, @@ -535,7 +535,7 @@ test("should allow when label is match", () => { callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.labels = { foo: "bar", @@ -564,7 +564,7 @@ test("should reject when annotation does not match", () => { }, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines annotations '.*' but Object carries '.*'./, @@ -591,7 +591,7 @@ test("should allow when annotation is match", () => { callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.annotations = { foo: "bar", @@ -621,7 +621,7 @@ test("should use `oldObject` when the operation is `DELETE`", () => { callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -646,7 +646,7 @@ test("should allow when deletionTimestamp is present on pod", () => { callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z"); pod.object.metadata.annotations = { @@ -678,7 +678,7 @@ test("should reject when deletionTimestamp is not present on pod", () => { callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.annotations = { foo: "bar", diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index 9604477d1..cc59c4567 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -35,6 +35,7 @@ import { mismatchedGroup, mismatchedVersion, mismatchedKind, + missingCarriableNamespace, unbindableNamespaces, uncarryableNamespace, } from "./adjudicators"; @@ -53,7 +54,7 @@ export function shouldSkipRequest( ignoredNamespaces?: string[], ): string { const prefix = "Ignoring Admission Callback:"; - const obj = req.operation === Operation.DELETE ? req.oldObject : req.object; + const obj = (req.operation === Operation.DELETE ? req.oldObject : req.object)!; // prettier-ignore return ( @@ -139,6 +140,12 @@ export function shouldSkipRequest( `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` ) : + missingCarriableNamespace(capabilityNamespaces, obj) ? + ( + `${prefix} Object does not carry a namespace ` + + `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` + ) : + "" ); } diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index 59c818424..714a58025 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -4,7 +4,12 @@ import { expect, describe, it } from "@jest/globals"; import { kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import * as fc from "fast-check"; -import { CreatePod, DeletePod } from "../../fixtures/loader"; +import { + AdmissionRequestCreateClusterRole, + AdmissionRequestCreateDeployment, + AdmissionRequestCreatePod, + AdmissionRequestDeletePod, +} from "../../fixtures/loader"; import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event } from "../enums"; @@ -12,6 +17,8 @@ import { Event } from "../enums"; export const callback = () => undefined; export const podKind = modelToGroupVersionKind(kind.Pod.name); +export const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); +export const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); const defaultFilters = { annotations: {}, @@ -30,6 +37,22 @@ const defaultBinding = { model: kind.Pod, }; +export const groupBinding = { + callback, + event: Event.Create, + filters: defaultFilters, + kind: deploymentKind, + model: kind.Deployment, +}; + +export const clusterScopedBinding = { + callback, + event: Event.Delete, + filters: defaultFilters, + kind: clusterRoleKind, + model: kind.ClusterRole, +}; + describe("when fuzzing shouldSkipRequest", () => { it("should handle random inputs without crashing", () => { fc.assert( @@ -123,9 +146,65 @@ describe("when fuzzing shouldSkipRequest", () => { describe("when checking specific properties of shouldSkipRequest()", () => {}); +describe("when a binding contains a group scoped object", () => { + const admissionRequestDeployment = AdmissionRequestCreateDeployment(); + const admissionRequestPod = AdmissionRequestCreatePod(); + it("should skip request when the group is different", () => { + expect(shouldSkipRequest(groupBinding, admissionRequestPod, [])).toMatch( + /Ignoring Admission Callback: Binding defines group '.+' but Request declares ''./, + ); + }); + it("should not skip request when the group is the same", () => { + const groupBindingNoRegex = { + ...groupBinding, + filters: { + ...groupBinding.filters, + regexName: "", + }, + }; + expect(shouldSkipRequest(groupBindingNoRegex, admissionRequestDeployment, [])).toMatch(""); + }); +}); + +describe("when a capability defines namespaces and the admission request object is cluster-scoped", () => { + const capabilityNamespaces = ["monitoring"]; + const admissionRequestCreateClusterRole = AdmissionRequestCreateClusterRole(); + it("should skip request when the capability namespace does not exist on the object", () => { + const binding = { + ...clusterScopedBinding, + event: Event.Create, + filters: { + ...clusterScopedBinding.filters, + regexName: "", + }, + }; + + expect(shouldSkipRequest(binding, admissionRequestCreateClusterRole, capabilityNamespaces)).toMatch( + /Ignoring Admission Callback: Object does not carry a namespace but namespaces allowed by Capability are '.+'./, + ); + }); +}); +describe("when a binding contains a cluster scoped object", () => { + const admissionRequestCreateClusterRole = AdmissionRequestCreateClusterRole(); + + it("should skip request when the binding defines a namespace on a cluster scoped object", () => { + const clusterScopedBindingWithNamespace = { + ...clusterScopedBinding, + event: Event.Create, + filters: { + ...clusterScopedBinding.filters, + namespaces: ["namespace"], + }, + }; + expect(shouldSkipRequest(clusterScopedBindingWithNamespace, admissionRequestCreateClusterRole, [])).toMatch( + /Ignoring Admission Callback: Binding defines namespaces '.+' but Object carries ''./, + ); + }); +}); + describe("when a pod is created", () => { it("should reject when regex name does not match", () => { - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(defaultBinding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name regex '.+' but Object carries '.+'./, ); @@ -134,7 +213,7 @@ describe("when a pod is created", () => { it("should not reject when regex name does match", () => { const filters = { ...defaultFilters, regexName: "^cool" }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -146,14 +225,14 @@ describe("when a pod is created", () => { }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); it("should reject when regex namespace does not match", () => { const filters = { ...defaultFilters, regexNamespaces: ["^argo"] }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespace regexes '.+' but Object carries '.+'./, ); @@ -161,13 +240,13 @@ describe("when a pod is created", () => { it("should not reject when namespace is not ignored", () => { const filters = { ...defaultFilters, regexName: "" }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch(""); }); it("should reject when namespace is ignored", () => { const filters = { ...defaultFilters, regexName: "" }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [], ["helm-releasename"])).toMatch( /Ignoring Admission Callback: Object carries namespace '.+' but ignored namespaces include '.+'./, ); @@ -178,7 +257,7 @@ describe("when a pod is deleted", () => { it("should reject when regex name does not match", () => { const filters = { ...defaultFilters, regexName: "^default$" }; const binding = { ...defaultBinding, filters }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name regex '.+' but Object carries '.+'./, ); @@ -187,7 +266,7 @@ describe("when a pod is deleted", () => { it("should not reject when regex name does match", () => { const filters = { ...defaultFilters, regexName: "^cool" }; const binding = { ...defaultBinding, filters }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -197,7 +276,7 @@ describe("when a pod is deleted", () => { ...defaultBinding, filters, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespace regexes '.+' but Object carries '.+'./, ); @@ -216,7 +295,7 @@ describe("when a pod is deleted", () => { ...defaultBinding, filters, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -226,7 +305,7 @@ describe("when a pod is deleted", () => { ...defaultBinding, filters, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name '.+' but Object carries '.+'./, ); @@ -238,7 +317,7 @@ describe("when a pod is deleted", () => { ...defaultBinding, filters, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [], ["helm-releasename"])).toMatch( /Ignoring Admission Callback: Object carries namespace '.+' but ignored namespaces include '.+'./, ); @@ -251,7 +330,7 @@ describe("when a pod is deleted", () => { filters, callback, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch(""); }); }); @@ -268,7 +347,7 @@ it("should reject when kind does not match", () => { filters, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines kind '.+' but Request declares 'Pod'./, @@ -287,7 +366,7 @@ it("should reject when group does not match", () => { filters, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines group '.+' but Request declares ''./, @@ -306,7 +385,7 @@ it("should reject when version does not match", () => { filters, callback, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines version '.+' but Request declares '.+'./, @@ -316,7 +395,7 @@ it("should reject when version does not match", () => { it("should allow when group, version, and kind match", () => { const filters = { ...defaultFilters, regexName: "" }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -325,7 +404,7 @@ it("should allow when kind match and others are empty", () => { const filters = { ...defaultFilters, regexName: "" }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -336,7 +415,7 @@ it("should reject when the capability namespace does not match", () => { ...defaultBinding, filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( /Ignoring Admission Callback: Object carries namespace '.+' but namespaces allowed by Capability are '.+'./, @@ -346,7 +425,7 @@ it("should reject when the capability namespace does not match", () => { it("should reject when namespace does not match", () => { const filters = { ...defaultFilters, namespaces: ["bleh"] }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines namespaces '.+' but Object carries '.+'./, @@ -367,7 +446,7 @@ it("should allow when namespace is match", () => { ...defaultBinding, filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -383,7 +462,7 @@ it("should reject when label does not match", () => { ...defaultBinding, filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines labels '.+' but Object carries '.+'./, @@ -405,7 +484,7 @@ it("should allow when label is match", () => { filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.labels = { foo: "bar", @@ -427,7 +506,7 @@ it("should reject when annotation does not match", () => { ...defaultBinding, filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines annotations '.+' but Object carries '.+'./, @@ -452,7 +531,7 @@ it("should allow when annotation is match", () => { filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.annotations = { foo: "bar", @@ -479,7 +558,7 @@ it("should use `oldObject` when the operation is `DELETE`", () => { filters, }; - const pod = DeletePod(); + const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); @@ -502,7 +581,7 @@ it("should allow when deletionTimestamp is present on pod", () => { filters, }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z"); pod.object.metadata.annotations = { @@ -526,7 +605,7 @@ it("should reject when deletionTimestamp is not present on pod", () => { }; const binding = { ...defaultBinding, filters }; - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; pod.object.metadata.annotations = { foo: "bar", @@ -548,19 +627,19 @@ describe("when multiple filters are triggered", () => { }; const binding = { ...defaultBinding, filters }; it("should display the failure message for the first matching filter", () => { - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( /Ignoring Admission Callback: Binding defines name 'not-a-match' but Object carries '.+'./, ); }); it("should NOT display the failure message for the second matching filter", () => { - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).not.toMatch( /Ignoring Admission Callback: Binding defines namespaces 'not-allowed,also-not-matching' but Object carries '.+'./, ); }); it("should NOT display the failure message for the third matching filter", () => { - const pod = CreatePod(); + const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).not.toMatch( /Ignoring Admission Callback: Binding defines name regex 'asdf' but Object carries '.*./, ); diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index d4ecc85c1..d2a49fee4 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1171,6 +1171,45 @@ describe("filterNoMatchReason", () => { }); }); + test("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { + const binding = { + kind: { kind: "ClusterRole" }, + }; + const obj = { + kind: "ClusterRole", + apiVersion: "rbac.authorization.k8s.io/v1", + metadata: { name: "clusterrole1" }, + }; + const capabilityNamespaces: string[] = ["monitoring"]; + const result = filterNoMatchReason( + binding as unknown as Partial, + obj as unknown as Partial, + capabilityNamespaces, + ); + expect(result).toEqual( + "Ignoring Watch Callback: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", + ); + }); + + test("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { + const binding = { + kind: { kind: "ClusterRole" }, + filters: { namespaces: ["ns1"] }, + }; + const obj = { + kind: "ClusterRole", + apiVersion: "rbac.authorization.k8s.io/v1", + metadata: { name: "clusterrole1" }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason( + binding as unknown as Partial, + obj as unknown as Partial, + capabilityNamespaces, + ); + expect(result).toEqual("Ignoring Watch Callback: Binding defines namespaces '[\"ns1\"]' but Object carries ''."); + }); + test("returns namespace filter error for namespace objects with namespace filters", () => { const binding = { kind: { kind: "Namespace" }, diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 769a96a9a..f401b87a1 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -26,6 +26,7 @@ import { mismatchedNameRegex, mismatchedNamespace, mismatchedNamespaceRegex, + missingCarriableNamespace, unbindableNamespaces, uncarryableNamespace, } from "./filter/adjudicators"; @@ -139,6 +140,12 @@ export function filterNoMatchReason( `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` ) : + missingCarriableNamespace(capabilityNamespaces, obj) ? + ( + `${prefix} Object does not carry a namespace ` + + `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` + ) : + "" ); }