diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79fde786..e47a6036 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: id-token: write contents: read actions: read - uses: slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml@v2.1.0 with: run-scripts: "set:version, ci, build" @@ -82,7 +82,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Publish package id: publish - uses: slsa-framework/slsa-github-generator/actions/nodejs/publish@5a775b367a56d5bd118a224a811bba288150a563 # v2.0.0 + uses: slsa-framework/slsa-github-generator/actions/nodejs/publish@9103ac683d00ceecdb1c21507a9c7a9983ef46f4 # v2.0.0 with: access: public node-auth-token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 341096ba..3760238f 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -23,6 +23,6 @@ jobs: with: fetch-depth: 0 - name: Default Secret Scanning - uses: trufflesecurity/trufflehog@a2a17cd73d74376209d6323c80a9a55b424e25b0 # main + uses: trufflesecurity/trufflehog@03e8af1075a7f7410664de9f6a1101268c9c8c92 # main with: extra_args: --debug --no-verification # Warn on potential violations diff --git a/package-lock.json b/package-lock.json index d7f44c0f..5b1faa47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "jest": "29.7.0", "js-yaml": "^4.1.0", "shellcheck": "^3.0.0", - "ts-jest": "29.2.5", + "ts-jest": "29.2.6", "undici": "^7.0.1" }, "engines": { @@ -9525,9 +9525,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10355,9 +10355,9 @@ } }, "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, "license": "MIT", "dependencies": { @@ -10368,7 +10368,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.1", "yargs-parser": "^21.1.1" }, "bin": { diff --git a/package.json b/package.json index 9d1dfdd7..3794a3c7 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "jest": "29.7.0", "js-yaml": "^4.1.0", "shellcheck": "^3.0.0", - "ts-jest": "29.2.5", + "ts-jest": "29.2.6", "undici": "^7.0.1" }, "overrides": { diff --git a/src/lib/assets/defaultTestObjects.ts b/src/lib/assets/defaultTestObjects.ts new file mode 100644 index 00000000..28814a98 --- /dev/null +++ b/src/lib/assets/defaultTestObjects.ts @@ -0,0 +1,533 @@ +import { GenericClass } from "kubernetes-fluent-client"; +import { Event } from "../enums"; +import { CapabilityExport } from "../types"; +import { describe, beforeEach, jest, it, expect } from "@jest/globals"; +import { V1PolicyRule as PolicyRule } from "@kubernetes/client-node"; +import fs from "fs"; +import { clusterRole } from "./rbac"; +import * as helpers from "../helpers"; + +export const mockCapabilities: CapabilityExport[] = [ + { + rbac: [ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["create", "get", "patch", "watch"], + }, + ], + bindings: [ + { + kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, + isWatch: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + { + rbac: [ + { + apiGroups: ["apiextensions.k8s.io"], + resources: ["customresourcedefinitions"], + verbs: ["patch", "create"], + }, + ], + bindings: [ + { + kind: { + group: "apiextensions.k8s.io", + version: "v1", + kind: "customresourcedefinition", + plural: "customresourcedefinitions", + }, + isWatch: false, + isFinalize: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + { + rbac: [ + { + apiGroups: [""], + resources: ["namespaces"], + verbs: ["watch"], + }, + ], + bindings: [ + { + kind: { group: "", version: "v1", kind: "namespace", plural: "namespaces" }, + isWatch: true, + isFinalize: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + { + rbac: [ + { + apiGroups: [""], + resources: ["configmaps"], + verbs: ["watch"], + }, + ], + bindings: [ + { + kind: { group: "", version: "v1", kind: "configmap", plural: "configmaps" }, + isWatch: true, + isFinalize: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, +]; +describe("RBAC generation", () => { + beforeEach(() => { + jest.clearAllMocks(); + const mockPackageJsonRBAC = {}; + + jest.spyOn(fs, "readFileSync").mockImplementation((path: unknown) => { + if (typeof path === "string" && path.includes("package.json")) { + return JSON.stringify({ rbac: mockPackageJsonRBAC }); + } + return "{}"; + }); + }); + + it("should generate correct ClusterRole rules in scoped mode", () => { + const result = clusterRole("test-role", mockCapabilities, "scoped", []); + + expect(result.rules).toEqual([ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["create", "get", "patch", "watch"], + }, + { + apiGroups: ["apiextensions.k8s.io"], + resources: ["customresourcedefinitions"], + verbs: ["patch", "create"], + }, + { + apiGroups: [""], + resources: ["namespaces"], + verbs: ["watch"], + }, + { + apiGroups: [""], + resources: ["configmaps"], + verbs: ["watch"], + }, + ]); + }); + + it("should generate a ClusterRole with wildcard rules when not in scoped mode", () => { + const expectedWildcardRules = [ + { + apiGroups: ["*"], + resources: ["*"], + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], + }, + ]; + + const result = clusterRole("test-role", mockCapabilities, "admin", []); + + expect(result.rules).toEqual(expectedWildcardRules); + }); + + it("should return an empty rules array when capabilities are empty in scoped mode", () => { + const result = clusterRole("test-role", [], "scoped", []); + + expect(result.rules).toEqual([]); + }); + + it("should include finalize verbs if isFinalize is true in scoped mode", () => { + const capabilitiesWithFinalize: CapabilityExport[] = [ + { + rbac: [ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["patch"], + }, + ], + bindings: [ + { + kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, + isWatch: false, + isFinalize: true, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + ]; + + const result = clusterRole( + "test-role", + capabilitiesWithFinalize, + "scoped", + capabilitiesWithFinalize.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), + ); + + expect(result.rules).toEqual([ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["patch"], + }, + { + apiGroups: ["apiextensions.k8s.io"], + resources: ["customresourcedefinitions"], + verbs: ["patch", "create"], + }, + ]); + }); + + it("should deduplicate verbs and resources in rules", () => { + const capabilitiesWithDuplicates: CapabilityExport[] = [ + { + rbac: [ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["create", "get"], + }, + ], + bindings: [ + { + kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, + isWatch: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + { + rbac: [ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["get", "patch"], + }, + ], + bindings: [ + { + kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, + isWatch: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + ]; + + const result = clusterRole( + "test-role", + capabilitiesWithDuplicates, + "scoped", + capabilitiesWithDuplicates.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), + ); + + // Filter out only the rules for 'pepr.dev' and 'peprstores' + const filteredRules = result.rules?.filter( + rule => rule.apiGroups?.includes("pepr.dev") && rule.resources?.includes("peprstores"), + ); + + expect(filteredRules).toEqual([ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["create", "get", "patch", "watch"], + }, + ]); + }); +}); +describe("clusterRole", () => { + // Mocking the readRBACFromPackageJson function to return null + jest.mock("./rbac", () => ({ + ...(jest.requireActual("./rbac") as object), + readRBACFromPackageJson: jest.fn(() => null), + })); + + // Mocking createRBACMap to isolate the behavior of clusterRole function + jest.mock("../helpers", () => ({ + ...(jest.requireActual("../helpers") as object), + createRBACMap: jest.fn(), + })); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it("should handle keys with less than 3 segments and set group to an empty string", () => { + jest.spyOn(helpers, "createRBACMap").mockReturnValue({ + nodes: { + plural: "nodes", + verbs: ["get"], + }, + }); + + const capabilitiesWithShortKey: CapabilityExport[] = [ + { + rbac: [ + { + apiGroups: [""], + resources: ["nodes"], + verbs: ["get"], + }, + ], + bindings: [ + { + kind: { group: "", version: "v1", kind: "node", plural: "nodes" }, + isWatch: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + ]; + + const result = clusterRole( + "test-role", + capabilitiesWithShortKey, + "scoped", + capabilitiesWithShortKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), + ); + + expect(result.rules).toEqual([ + { + apiGroups: [""], + resources: ["nodes"], + verbs: ["get"], + }, + ]); + }); + + it("should handle keys with 3 or more segments and set group correctly", () => { + jest.spyOn(helpers, "createRBACMap").mockReturnValue({ + "apps/v1/deployments": { + plural: "deployments", + verbs: ["create"], + }, + }); + + const capabilitiesWithLongKey: CapabilityExport[] = [ + { + rbac: [ + { + apiGroups: ["apps"], + resources: ["deployments"], + verbs: ["create"], + }, + ], + bindings: [ + { + kind: { group: "apps", version: "v1", kind: "deployment", plural: "deployments" }, + isWatch: false, + event: Event.CREATE, + model: {} as GenericClass, + filters: { + name: "", + regexName: "", + namespaces: [], + regexNamespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + }, + ], + hasSchedule: false, + name: "", + description: "", + }, + ]; + + const result = clusterRole( + "test-role", + capabilitiesWithLongKey, + "scoped", + capabilitiesWithLongKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), + ); + + expect(result.rules).toEqual([ + { + apiGroups: ["apps"], + resources: ["deployments"], + verbs: ["create"], + }, + ]); + }); + + it("should handle non-array custom RBAC by defaulting to an empty array", () => { + // Mock readRBACFromPackageJson to return a non-array value + jest.spyOn(fs, "readFileSync").mockImplementation(() => { + return JSON.stringify({ + pepr: { + rbac: "not-an-array", // Simulate invalid RBAC structure + }, + }); + }); + + const result = clusterRole( + "test-role", + mockCapabilities, + "scoped", + mockCapabilities.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), + ); + + // The result should only contain rules from the capabilities, not from the invalid custom RBAC + expect(result.rules).toEqual([ + { + apiGroups: ["pepr.dev"], + resources: ["peprstores"], + verbs: ["create", "get", "patch", "watch"], + }, + { + apiGroups: ["apiextensions.k8s.io"], + resources: ["customresourcedefinitions"], + verbs: ["patch", "create"], + }, + { + apiGroups: [""], + resources: ["namespaces"], + verbs: ["watch"], + }, + { + apiGroups: [""], + resources: ["configmaps"], + verbs: ["watch"], + }, + ]); + }); + + it("should default to an empty verbs array if rule.verbs is undefined", () => { + // Simulate a custom RBAC rule with empty verbs + const customRbacWithNoVerbs: PolicyRule[] = [ + { + apiGroups: ["pepr.dev"], + resources: ["customresources"], + verbs: [], // Set verbs to an empty array to satisfy the V1PolicyRule type + }, + ]; + + jest.spyOn(fs, "readFileSync").mockImplementation(() => { + return JSON.stringify({ + pepr: { + rbac: customRbacWithNoVerbs, + }, + }); + }); + + const result = clusterRole("test-role", mockCapabilities, "scoped", customRbacWithNoVerbs); + + // Check that the verbs array is empty for the custom RBAC rule + expect(result.rules).toContainEqual({ + apiGroups: ["pepr.dev"], + resources: ["customresources"], + verbs: [], + }); + }); +}); diff --git a/src/lib/assets/rbac.test.ts b/src/lib/assets/rbac.test.ts index 9a630b84..d465f4fc 100644 --- a/src/lib/assets/rbac.test.ts +++ b/src/lib/assets/rbac.test.ts @@ -1,339 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { clusterRole, clusterRoleBinding, storeRole, serviceAccount, storeRoleBinding } from "./rbac"; -import { CapabilityExport } from "../types"; import { it, describe, expect, beforeEach, jest } from "@jest/globals"; -import { GenericClass } from "kubernetes-fluent-client"; import { V1PolicyRule as PolicyRule } from "@kubernetes/client-node"; -import { Event } from "../enums"; import fs from "fs"; -import * as helpers from "../helpers"; - -const mockCapabilities: CapabilityExport[] = [ - { - rbac: [ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["create", "get", "patch", "watch"], - }, - ], - bindings: [ - { - kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, - isWatch: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - { - rbac: [ - { - apiGroups: ["apiextensions.k8s.io"], - resources: ["customresourcedefinitions"], - verbs: ["patch", "create"], - }, - ], - bindings: [ - { - kind: { - group: "apiextensions.k8s.io", - version: "v1", - kind: "customresourcedefinition", - plural: "customresourcedefinitions", - }, - isWatch: false, - isFinalize: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - { - rbac: [ - { - apiGroups: [""], - resources: ["namespaces"], - verbs: ["watch"], - }, - ], - bindings: [ - { - kind: { group: "", version: "v1", kind: "namespace", plural: "namespaces" }, - isWatch: true, - isFinalize: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - { - rbac: [ - { - apiGroups: [""], - resources: ["configmaps"], - verbs: ["watch"], - }, - ], - bindings: [ - { - kind: { group: "", version: "v1", kind: "configmap", plural: "configmaps" }, - isWatch: true, - isFinalize: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, -]; - -describe("RBAC generation", () => { - beforeEach(() => { - jest.clearAllMocks(); - const mockPackageJsonRBAC = {}; - - jest.spyOn(fs, "readFileSync").mockImplementation((path: unknown) => { - if (typeof path === "string" && path.includes("package.json")) { - return JSON.stringify({ rbac: mockPackageJsonRBAC }); - } - return "{}"; - }); - }); - - it("should generate correct ClusterRole rules in scoped mode", () => { - const result = clusterRole("test-role", mockCapabilities, "scoped", []); - - expect(result.rules).toEqual([ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["create", "get", "patch", "watch"], - }, - { - apiGroups: ["apiextensions.k8s.io"], - resources: ["customresourcedefinitions"], - verbs: ["patch", "create"], - }, - { - apiGroups: [""], - resources: ["namespaces"], - verbs: ["watch"], - }, - { - apiGroups: [""], - resources: ["configmaps"], - verbs: ["watch"], - }, - ]); - }); - - it("should generate a ClusterRole with wildcard rules when not in scoped mode", () => { - const expectedWildcardRules = [ - { - apiGroups: ["*"], - resources: ["*"], - verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], - }, - ]; - - const result = clusterRole("test-role", mockCapabilities, "admin", []); - - expect(result.rules).toEqual(expectedWildcardRules); - }); - - it("should return an empty rules array when capabilities are empty in scoped mode", () => { - const result = clusterRole("test-role", [], "scoped", []); - - expect(result.rules).toEqual([]); - }); - - it("should include finalize verbs if isFinalize is true in scoped mode", () => { - const capabilitiesWithFinalize: CapabilityExport[] = [ - { - rbac: [ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["patch"], - }, - ], - bindings: [ - { - kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, - isWatch: false, - isFinalize: true, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - ]; - - const result = clusterRole( - "test-role", - capabilitiesWithFinalize, - "scoped", - capabilitiesWithFinalize.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), - ); - - expect(result.rules).toEqual([ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["patch"], - }, - { - apiGroups: ["apiextensions.k8s.io"], - resources: ["customresourcedefinitions"], - verbs: ["patch", "create"], - }, - ]); - }); - - it("should deduplicate verbs and resources in rules", () => { - const capabilitiesWithDuplicates: CapabilityExport[] = [ - { - rbac: [ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["create", "get"], - }, - ], - bindings: [ - { - kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, - isWatch: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - { - rbac: [ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["get", "patch"], - }, - ], - bindings: [ - { - kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, - isWatch: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - ]; - - const result = clusterRole( - "test-role", - capabilitiesWithDuplicates, - "scoped", - capabilitiesWithDuplicates.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), - ); - - // Filter out only the rules for 'pepr.dev' and 'peprstores' - const filteredRules = result.rules?.filter( - rule => rule.apiGroups?.includes("pepr.dev") && rule.resources?.includes("peprstores"), - ); - - expect(filteredRules).toEqual([ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["create", "get", "patch", "watch"], - }, - ]); - }); -}); +import { mockCapabilities } from "./defaultTestObjects"; describe("RBAC generation with mocked package.json", () => { beforeEach(() => { @@ -486,204 +157,3 @@ describe("storeRoleBinding", () => { expect(result).toEqual(expectedRoleBinding); }); }); - -describe("clusterRole", () => { - // Mocking the readRBACFromPackageJson function to return null - jest.mock("./rbac", () => ({ - ...(jest.requireActual("./rbac") as object), - readRBACFromPackageJson: jest.fn(() => null), - })); - - // Mocking createRBACMap to isolate the behavior of clusterRole function - jest.mock("../helpers", () => ({ - ...(jest.requireActual("../helpers") as object), - createRBACMap: jest.fn(), - })); - - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it("should handle keys with less than 3 segments and set group to an empty string", () => { - jest.spyOn(helpers, "createRBACMap").mockReturnValue({ - nodes: { - plural: "nodes", - verbs: ["get"], - }, - }); - - const capabilitiesWithShortKey: CapabilityExport[] = [ - { - rbac: [ - { - apiGroups: [""], - resources: ["nodes"], - verbs: ["get"], - }, - ], - bindings: [ - { - kind: { group: "", version: "v1", kind: "node", plural: "nodes" }, - isWatch: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - ]; - - const result = clusterRole( - "test-role", - capabilitiesWithShortKey, - "scoped", - capabilitiesWithShortKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), - ); - - expect(result.rules).toEqual([ - { - apiGroups: [""], - resources: ["nodes"], - verbs: ["get"], - }, - ]); - }); - - it("should handle keys with 3 or more segments and set group correctly", () => { - jest.spyOn(helpers, "createRBACMap").mockReturnValue({ - "apps/v1/deployments": { - plural: "deployments", - verbs: ["create"], - }, - }); - - const capabilitiesWithLongKey: CapabilityExport[] = [ - { - rbac: [ - { - apiGroups: ["apps"], - resources: ["deployments"], - verbs: ["create"], - }, - ], - bindings: [ - { - kind: { group: "apps", version: "v1", kind: "deployment", plural: "deployments" }, - isWatch: false, - event: Event.CREATE, - model: {} as GenericClass, - filters: { - name: "", - regexName: "", - namespaces: [], - regexNamespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - }, - ], - hasSchedule: false, - name: "", - description: "", - }, - ]; - - const result = clusterRole( - "test-role", - capabilitiesWithLongKey, - "scoped", - capabilitiesWithLongKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), - ); - - expect(result.rules).toEqual([ - { - apiGroups: ["apps"], - resources: ["deployments"], - verbs: ["create"], - }, - ]); - }); - - it("should handle non-array custom RBAC by defaulting to an empty array", () => { - // Mock readRBACFromPackageJson to return a non-array value - jest.spyOn(fs, "readFileSync").mockImplementation(() => { - return JSON.stringify({ - pepr: { - rbac: "not-an-array", // Simulate invalid RBAC structure - }, - }); - }); - - const result = clusterRole( - "test-role", - mockCapabilities, - "scoped", - mockCapabilities.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined), - ); - - // The result should only contain rules from the capabilities, not from the invalid custom RBAC - expect(result.rules).toEqual([ - { - apiGroups: ["pepr.dev"], - resources: ["peprstores"], - verbs: ["create", "get", "patch", "watch"], - }, - { - apiGroups: ["apiextensions.k8s.io"], - resources: ["customresourcedefinitions"], - verbs: ["patch", "create"], - }, - { - apiGroups: [""], - resources: ["namespaces"], - verbs: ["watch"], - }, - { - apiGroups: [""], - resources: ["configmaps"], - verbs: ["watch"], - }, - ]); - }); - - it("should default to an empty verbs array if rule.verbs is undefined", () => { - // Simulate a custom RBAC rule with empty verbs - const customRbacWithNoVerbs: PolicyRule[] = [ - { - apiGroups: ["pepr.dev"], - resources: ["customresources"], - verbs: [], // Set verbs to an empty array to satisfy the V1PolicyRule type - }, - ]; - - jest.spyOn(fs, "readFileSync").mockImplementation(() => { - return JSON.stringify({ - pepr: { - rbac: customRbacWithNoVerbs, - }, - }); - }); - - const result = clusterRole("test-role", mockCapabilities, "scoped", customRbacWithNoVerbs); - - // Check that the verbs array is empty for the custom RBAC rule - expect(result.rules).toContainEqual({ - apiGroups: ["pepr.dev"], - resources: ["customresources"], - verbs: [], - }); - }); -}); diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index e04043b0..48e02dc2 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -662,6 +662,41 @@ describe("definedCallback", () => { [{ isMutate: true, mutateCallback }, mutateCallback], [{ isWatch: true, watchCallback }, watchCallback], [{ isFinalize: true, finalizeCallback }, finalizeCallback], + [{ isFinalize: true, isValidate: true, finalizeCallback, validateCallback }, finalizeCallback], // Finalize > Validate + [{ isFinalize: true, isWatch: true, finalizeCallback, watchCallback }, finalizeCallback], // Finalize > Watch + [ + { isFinalize: true, isMutate: true, isWatch: true, finalizeCallback, mutateCallback, watchCallback }, + finalizeCallback, + ], // Finalize > Mutate > Watch + [{ isValidate: true, isMutate: true, validateCallback, mutateCallback }, mutateCallback], // Mutate > Validate + [{ isWatch: true, isMutate: true, watchCallback, mutateCallback }, watchCallback], // Watch > Mutate + [{ isMutate: true, isFinalize: true, mutateCallback, finalizeCallback }, finalizeCallback], // Finalize > Mutate + [ + { isMutate: true, isWatch: true, isFinalize: true, mutateCallback, watchCallback, finalizeCallback }, + finalizeCallback, + ], // Finalize > Watch > Mutate + [ + { isValidate: true, isMutate: true, isFinalize: true, validateCallback, mutateCallback, finalizeCallback }, + finalizeCallback, + ], // Finalize > Mutate > Validate + [ + { isValidate: true, isMutate: true, isWatch: true, validateCallback, mutateCallback, watchCallback }, + watchCallback, + ], // Watch > Mutate > Validate + [{ isValidate: true, isWatch: true, validateCallback, watchCallback }, watchCallback], // Watch > Validate + [ + { + isValidate: true, + isMutate: true, + isWatch: true, + isFinalize: true, + validateCallback, + mutateCallback, + watchCallback, + finalizeCallback, + }, + finalizeCallback, + ], // Finalize > Watch > Mutate > Validate ])("given %j, returns %s", (given, expected) => { const binding: Binding = { ...defaultBinding, diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index 4d4d7601..ed1d3afa 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -137,8 +137,9 @@ describe("shouldSkipRequest", () => { }); const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines name regex '\^default\$' but Object carries 'cool-name-podinfo-66bbff7cf4-fwhl2'./, ); }); @@ -160,7 +161,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines name regex '\^default\$' but Object carries 'cool-name-podinfo-66bbff7cf4-fwhl2'./, ); }); @@ -192,7 +193,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines namespace regexes '\["\^argo"\]' but Object carries 'helm-releasename'./, ); }); @@ -204,7 +205,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines namespace regexes '\["\^argo"\]' but Object carries 'helm-releasename'./, ); }); @@ -226,7 +227,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines name 'bleh' but Object carries 'cool-name-podinfo-66bbff7cf4-fwhl2'./, ); }); @@ -239,7 +240,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines kind '.*' but Request declares '.*'./, + /Ignoring Admission Callback: Binding defines kind 'Nope' but Request declares 'Pod'./, ); }); @@ -252,7 +253,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines group '.*' but Request declares '.*'./, + /Ignoring Admission Callback: Binding defines group 'Nope' but Request declares ''./, ); }); @@ -265,7 +266,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines version '.*' but Request declares '.*'./, + /Ignoring Admission Callback: Binding defines version 'Nope' but Request declares 'v1'./, ); }); @@ -298,7 +299,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( - /Ignoring Admission Callback: Object carries namespace '.*' but namespaces allowed by Capability are '.*'./, + /Ignoring Admission Callback: Object carries namespace 'helm-releasename' but namespaces allowed by Capability are '\["bleh","bleh2"\]'\./, ); }); @@ -311,7 +312,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespaces '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines namespaces '\["bleh"\]' but Object carries 'helm-releasename'./, ); }); @@ -339,7 +340,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines labels '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines labels '\{"foo":"bar"\}' but Object carries '\{"app\.kubernetes\.io\/name":"cool-name-podinfo","pod-template-hash":"66bbff7cf4","zarf-agent":"patched","test-op":"create"\}'.*/, ); }); @@ -378,7 +379,7 @@ describe("shouldSkipRequest", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines annotations '.*' but Object carries '.*'./, + /Ignoring Admission Callback: Binding defines annotations '\{"foo":"bar"\}' but Object carries '\{"prometheus\.io\/port":"9898","prometheus\.io\/scrape":"true"\}'./, ); });