diff --git a/src/pepr/operator/controllers/exemptions/exemption-store.spec.ts b/src/pepr/operator/controllers/exemptions/exemption-store.spec.ts new file mode 100644 index 000000000..15e25d948 --- /dev/null +++ b/src/pepr/operator/controllers/exemptions/exemption-store.spec.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { Matcher, MatcherKind, Policy } from "../../crd"; +import { ExemptionStore } from "./exemption-store"; + +const enforcerMatcher = { + namespace: "neuvector", + name: "^neuvector-enforcer-pod.*", + kind: MatcherKind.Pod, +}; + +const controllerMatcher = { + namespace: "neuvector", + name: "^neuvector-controller-pod.*", + kind: MatcherKind.Pod, +}; + +const getExemption = (uid: string, matcher: Matcher, policies: Policy[]) => { + return { + metadata: { + uid, + }, + spec: { + exemptions: [ + { + matcher, + policies, + }, + ], + }, + }; +}; + +describe("Exemption Store", () => { + beforeEach(() => { + ExemptionStore.init(); + }); + + it("Add exemption", async () => { + const e = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]); + ExemptionStore.add(e); + const matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + + expect(matchers).toHaveLength(1); + }); + + it("Delete exemption", async () => { + const e = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]); + ExemptionStore.add(e); + let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(1); + ExemptionStore.remove(e); + + matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(0); + }); + + it("Update exemption", async () => { + const enforcerException = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]); + ExemptionStore.add(enforcerException); + + let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(1); + + const controllerExemption = getExemption("uid", controllerMatcher, [Policy.RequireNonRootUser]); + ExemptionStore.add(controllerExemption); + + matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(0); + }); + + it("Add multiple policies", async () => { + const enforcerException = getExemption("foo", enforcerMatcher, [Policy.DisallowPrivileged]); + ExemptionStore.add(enforcerException); + + let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(1); + + const controllerExemption = getExemption("bar", controllerMatcher, [Policy.RequireNonRootUser]); + ExemptionStore.add(controllerExemption); + + matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(1); + + matchers = ExemptionStore.getByPolicy(Policy.RequireNonRootUser); + expect(matchers).toHaveLength(1); + }); + + it("Add duplicate exemptions owned by different owners", async () => { + const enforcerException = getExemption("foo", enforcerMatcher, [Policy.DisallowPrivileged]); + const otherEnforcerException = getExemption("bar", enforcerMatcher, [ + Policy.DisallowPrivileged, + ]); + ExemptionStore.add(enforcerException); + ExemptionStore.add(otherEnforcerException); + + const matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged); + expect(matchers).toHaveLength(2); + }); +}); diff --git a/src/pepr/operator/controllers/exemptions/exemption-store.ts b/src/pepr/operator/controllers/exemptions/exemption-store.ts new file mode 100644 index 000000000..3cb024de8 --- /dev/null +++ b/src/pepr/operator/controllers/exemptions/exemption-store.ts @@ -0,0 +1,83 @@ +import { Log } from "pepr"; +import { StoredMatcher } from "../../../policies"; +import { Matcher, Policy, UDSExemption } from "../../crd"; + +export type PolicyOwnerMap = Map; +export type PolicyMap = Map; +let policyExemptionMap: PolicyMap; +let policyOwnerMap: PolicyOwnerMap; + +function init(): void { + policyExemptionMap = new Map(); + policyOwnerMap = new Map(); + for (const p of Object.values(Policy)) { + policyExemptionMap.set(p, []); + } +} + +function getByPolicy(policy: Policy): StoredMatcher[] { + return policyExemptionMap.get(policy) || []; +} + +function setByPolicy(policy: Policy, matchers: StoredMatcher[]): void { + policyExemptionMap.set(policy, matchers); +} + +function addMatcher(matcher: Matcher, p: Policy, owner: string = ""): void { + const storedMatcher = { + ...matcher, + owner, + }; + + const storedMatchers = getByPolicy(p); + storedMatchers.push(storedMatcher); +} + +// Iterate through each exemption block of CR and add matchers to PolicyMap +function add(exemption: UDSExemption, log: boolean = true) { + // Remove any existing exemption for this owner, in case of WatchPhase.Modified + remove(exemption); + const owner = exemption.metadata?.uid || ""; + policyOwnerMap.set(owner, exemption); + + for (const e of exemption.spec?.exemptions ?? []) { + const policies = e.policies ?? []; + for (const p of policies) { + // Append the matcher to the list of stored matchers for this policy + addMatcher(e.matcher, p, owner); + if (log) { + Log.debug(`Added exemption to ${p}: ${JSON.stringify(e.matcher)}`); + } + } + } +} + +function remove(exemption: UDSExemption) { + const owner = exemption.metadata?.uid || ""; + const prevExemption = policyOwnerMap.get(owner); + + if (prevExemption) { + for (const e of prevExemption.spec?.exemptions ?? []) { + const policies = e.policies ?? []; + for (const p of policies) { + const existingMatchers = getByPolicy(p); + const filteredList = existingMatchers.filter(m => { + return m.owner !== owner; + }); + setByPolicy(p, filteredList); + } + } + policyOwnerMap.delete(owner); + Log.debug(`Removed all policy exemptions for ${owner}`); + } else { + Log.debug(`No existing exemption for owner ${owner}`); + } +} + +// export object with all included export as properties +export const ExemptionStore = { + init, + add, + remove, + getByPolicy, +}; diff --git a/src/pepr/operator/controllers/exemptions/exemptions.spec.ts b/src/pepr/operator/controllers/exemptions/exemptions.spec.ts index d6bc4f265..8c276d879 100644 --- a/src/pepr/operator/controllers/exemptions/exemptions.spec.ts +++ b/src/pepr/operator/controllers/exemptions/exemptions.spec.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it } from "@jest/globals"; -import { PolicyMap } from "../../../policies"; import { MatcherKind, Policy } from "../../crd"; import { Exemption } from "../../crd/generated/exemption-v1alpha1"; -import { WatchPhase, processExemptions, setupMap } from "./exemptions"; +import { ExemptionStore } from "./exemption-store"; +import { WatchPhase, processExemptions } from "./exemptions"; const enforcerMatcher = { namespace: "neuvector", @@ -52,21 +52,19 @@ const neuvectorMockExemption = { }, } as Exemption; -let exemptionMap: PolicyMap; - describe("Test processExemptions() no duplicate matchers in same CR", () => { beforeEach(() => { - exemptionMap = setupMap(); + ExemptionStore.init(); }); it("Add exemptions for the first time", async () => { - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + processExemptions(neuvectorMockExemption, WatchPhase.Added); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, storedPrometheusMatcher, @@ -74,13 +72,13 @@ describe("Test processExemptions() no duplicate matchers in same CR", () => { }); it("Does not re-add matchers on updates", async () => { - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(neuvectorMockExemption, WatchPhase.Modified, exemptionMap); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + processExemptions(neuvectorMockExemption, WatchPhase.Added); + processExemptions(neuvectorMockExemption, WatchPhase.Modified); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, storedPrometheusMatcher, @@ -118,20 +116,22 @@ describe("Test processExemptions() no duplicate matchers in same CR", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(updatedNeuvectorExemption, WatchPhase.Modified, exemptionMap); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([ + processExemptions(neuvectorMockExemption, WatchPhase.Added); + processExemptions(updatedNeuvectorExemption, WatchPhase.Modified); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([ { ...storedPromtailMatcher, owner: exemption1UID }, ]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, ]); - expect(exemptionMap.get(Policy.DisallowHostNamespaces)).toEqual([storedControllerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowHostNamespaces)).toEqual([ + storedControllerMatcher, + ]); }); it("Adds duplicate exemptions set by same CR if different matcher kind", async () => { @@ -157,12 +157,12 @@ describe("Test processExemptions() no duplicate matchers in same CR", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption2, WatchPhase.Added, exemptionMap); + processExemptions(neuvectorMockExemption2, WatchPhase.Added); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowNodePortServices)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowNodePortServices)).toEqual([ { ...storedEnforcerMatcher, kind: MatcherKind.Service }, ]); }); @@ -195,23 +195,23 @@ describe("Test processExemptions() no duplicate matchers in same CR", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption2, WatchPhase.Added, exemptionMap); + processExemptions(neuvectorMockExemption2, WatchPhase.Added); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, { ...storedEnforcerMatcher, namespace: diffNS, }, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([ storedEnforcerMatcher, { ...storedEnforcerMatcher, namespace: diffNS, }, ]); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([ storedEnforcerMatcher, { ...storedEnforcerMatcher, @@ -244,23 +244,23 @@ describe("Test processExemptions() no duplicate matchers in same CR", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption2, WatchPhase.Added, exemptionMap); + processExemptions(neuvectorMockExemption2, WatchPhase.Added); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, { ...storedEnforcerMatcher, namespace: diffNS, }, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); }); }); describe("Test processExemptions() duplicate matchers in same CR", () => { beforeEach(() => { - exemptionMap = setupMap(); + ExemptionStore.init(); }); const sameMatcherMockExemption = { @@ -286,22 +286,22 @@ describe("Test processExemptions() duplicate matchers in same CR", () => { }; it("Adds same matchers with different policies", () => { - processExemptions(sameMatcherMockExemption, WatchPhase.Added, exemptionMap); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + processExemptions(sameMatcherMockExemption, WatchPhase.Added); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); }); it("Does not re-add matchers on updates", () => { - processExemptions(sameMatcherMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(sameMatcherMockExemption, WatchPhase.Modified, exemptionMap); + processExemptions(sameMatcherMockExemption, WatchPhase.Added); + processExemptions(sameMatcherMockExemption, WatchPhase.Modified); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); }); - it("Handles updates - remove policy, remove matcher, add policy, add matcher", async () => { + it.only("Handles updates - remove policy, remove matcher, add policy, add matcher", async () => { // remove RequireNonRoot from enforcerMatcher (satisfies remove matcher in this duplicate case) // add DisallowHostNamespaces to enforcerMatcher // add controllerMatcher with DisallowPrivileged @@ -331,29 +331,31 @@ describe("Test processExemptions() duplicate matchers in same CR", () => { }, } as Exemption; - processExemptions(sameMatcherMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(updateSameMatcherMock, WatchPhase.Modified, exemptionMap); + processExemptions(sameMatcherMockExemption, WatchPhase.Added); + processExemptions(updateSameMatcherMock, WatchPhase.Modified); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([ storedEnforcerMatcher, storedControllerMatcher, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowHostNamespaces)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowHostNamespaces)).toEqual([ + storedEnforcerMatcher, + ]); }); }); describe("Test processExemptions(); phase DELETED", () => { beforeEach(() => { - exemptionMap = setupMap(); + ExemptionStore.init(); }); it("Removes all CRs exemptions when deleted", async () => { - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(neuvectorMockExemption, WatchPhase.Deleted, exemptionMap); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([]); + processExemptions(neuvectorMockExemption, WatchPhase.Added); + processExemptions(neuvectorMockExemption, WatchPhase.Deleted); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([]); }); it("Does not remove exemptions set by separate CR from the one being deleted", async () => { @@ -375,13 +377,13 @@ describe("Test processExemptions(); phase DELETED", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(promtailMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(neuvectorMockExemption, WatchPhase.Deleted, exemptionMap); + processExemptions(neuvectorMockExemption, WatchPhase.Added); + processExemptions(promtailMockExemption, WatchPhase.Added); + processExemptions(neuvectorMockExemption, WatchPhase.Deleted); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedPromtailMatcher]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedPromtailMatcher]); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedPromtailMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedPromtailMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedPromtailMatcher]); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedPromtailMatcher]); }); it("Does not delete duplicate exemptions if set by separate CRs", async () => { @@ -421,13 +423,13 @@ describe("Test processExemptions(); phase DELETED", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption2, WatchPhase.Added, exemptionMap); - processExemptions(neuvectorDuplicateMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(neuvectorDuplicateMockExemption, WatchPhase.Deleted, exemptionMap); + processExemptions(neuvectorMockExemption2, WatchPhase.Added); + processExemptions(neuvectorDuplicateMockExemption, WatchPhase.Added); + processExemptions(neuvectorDuplicateMockExemption, WatchPhase.Deleted); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([storedEnforcerMatcher]); }); it("Does not delete exemptions for the same policies from separate CRs during modification", async () => { @@ -473,15 +475,15 @@ describe("Test processExemptions(); phase DELETED", () => { }, } as Exemption; - processExemptions(neuvectorMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(promtailMockExemption, WatchPhase.Added, exemptionMap); - processExemptions(promtailUpdatedMockExemption, WatchPhase.Modified, exemptionMap); + processExemptions(neuvectorMockExemption, WatchPhase.Added); + processExemptions(promtailMockExemption, WatchPhase.Added); + processExemptions(promtailUpdatedMockExemption, WatchPhase.Modified); - expect(exemptionMap.get(Policy.RequireNonRootUser)).toEqual([ + expect(ExemptionStore.getByPolicy(Policy.RequireNonRootUser)).toEqual([ storedEnforcerMatcher, storedPromtailMatcher, ]); - expect(exemptionMap.get(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); - expect(exemptionMap.get(Policy.DisallowPrivileged)).toEqual([storedPromtailMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DropAllCapabilities)).toEqual([storedEnforcerMatcher]); + expect(ExemptionStore.getByPolicy(Policy.DisallowPrivileged)).toEqual([storedPromtailMatcher]); }); }); diff --git a/src/pepr/operator/controllers/exemptions/exemptions.ts b/src/pepr/operator/controllers/exemptions/exemptions.ts index 2719d9eb6..a52a39723 100644 --- a/src/pepr/operator/controllers/exemptions/exemptions.ts +++ b/src/pepr/operator/controllers/exemptions/exemptions.ts @@ -1,6 +1,5 @@ -import { Log, R } from "pepr"; -import { PolicyMap } from "../../../policies"; -import { Policy, UDSExemption } from "../../crd"; +import { UDSExemption } from "../../crd"; +import { ExemptionStore } from "./exemption-store"; export enum WatchPhase { Added = "ADDED", @@ -10,100 +9,16 @@ export enum WatchPhase { Error = "ERROR", } -// Iterate through each exemption block of CR and add matchers to PolicyMap -function addToMap(map: PolicyMap, exemption: UDSExemption, log: boolean = true) { - const exemptions = exemption.spec?.exemptions ?? []; - for (const e of exemptions) { - const matcherToStore = { - ...e.matcher, - owner: exemption.metadata?.uid || "", - }; - - const policies = e.policies ?? []; - for (const p of policies) { - // Append the matcher to the list of stored matchers for this policy - const storedMatchers = map.get(p) || []; - storedMatchers.push(matcherToStore); - map.set(p, storedMatchers); - if (log) { - Log.debug(`Added exemption to ${p}: ${JSON.stringify(matcherToStore)}`); - } - } - } -} - -// Update the PolicyMap, adding or subtracting matchers based on comparison of maps -function compareAndMerge(tempMap: PolicyMap, realMap: PolicyMap, owner: string) { - for (const [policy, currentMatchers] of realMap.entries()) { - const incomingMatchers = tempMap.get(policy) || []; - const mergedMatchers = []; - - for (const cm of currentMatchers) { - // Add current matchers back to map if they exists in the new list - if (R.includes(cm, incomingMatchers)) { - mergedMatchers.push(cm); - } - // Add all matchers owned by a different owner - if (cm.owner !== owner) { - mergedMatchers.push(cm); - } - } - - // Combine new matchers with old, ignoring duplicates - const newMatchers = R.union(mergedMatchers, incomingMatchers); - - // Only update the map if there are diffs - if (!R.equals(currentMatchers, newMatchers)) { - realMap.set(policy, newMatchers); - Log.debug(`Updated exemptions for ${policy}: ${JSON.stringify(newMatchers)}`); - } - } -} - -export function setupMap() { - const policyList = Object.values(Policy); - const tempMap: PolicyMap = new Map(); - for (const p of policyList) { - tempMap.set(p, []); - } - return tempMap; -} - // Handle adding, updating, and deleting exemptions from Policymap -export function processExemptions( - exempt: UDSExemption, - phase: WatchPhase, - exemptionMap: PolicyMap, -) { - const exemptionOwner = exempt.metadata?.uid || ""; +export function processExemptions(exemption: UDSExemption, phase: WatchPhase) { switch (phase) { case WatchPhase.Added: - addToMap(exemptionMap, exempt); + case WatchPhase.Modified: + ExemptionStore.add(exemption); break; - case WatchPhase.Modified: { - const tempMap = setupMap(); - addToMap(tempMap, exempt, false); - compareAndMerge(tempMap, exemptionMap, exemptionOwner); - break; - } - case WatchPhase.Deleted: - removeExemptions(exempt, exemptionMap); + ExemptionStore.remove(exemption); break; } } - -export function removeExemptions(exempt: UDSExemption, exemptionMap: PolicyMap) { - // Loop through exemptions and remove matchers from policies in the local map - for (const e of exempt.spec?.exemptions ?? []) { - for (const p of e.policies) { - const matchers = exemptionMap.get(p) || []; - const filteredList = matchers.filter(m => { - if (!R.equals(m, { ...e.matcher, owner: exempt.metadata?.uid || "" })) return m; - }); - exemptionMap.set(p, filteredList); - } - } - Log.debug(`Removed all policy exemptions for ${exempt.metadata?.name}`); -} diff --git a/src/pepr/policies/common.ts b/src/pepr/policies/common.ts index bc642b8d0..0a953faa4 100644 --- a/src/pepr/policies/common.ts +++ b/src/pepr/policies/common.ts @@ -1,6 +1,5 @@ import { KubernetesObject, V1Container, V1SecurityContext } from "@kubernetes/client-node"; import { Capability, PeprMutateRequest, PeprValidateRequest, a } from "pepr"; -import { PolicyMap } from "."; import { Policy } from "../operator/crd"; export type Ctx = { @@ -15,7 +14,6 @@ export const policies = new Capability({ }); export const { When } = policies; -export const policyExemptionMap: PolicyMap = new Map(); // Returns all volumes in the pod export function volumes(request: PeprValidateRequest) { diff --git a/src/pepr/policies/exemptions/index.spec.ts b/src/pepr/policies/exemptions/index.spec.ts index 297fe1d34..2ab36dd25 100644 --- a/src/pepr/policies/exemptions/index.spec.ts +++ b/src/pepr/policies/exemptions/index.spec.ts @@ -1,12 +1,13 @@ import { beforeAll, describe, expect, it, jest } from "@jest/globals"; import { PeprValidateRequest, kind } from "pepr"; import { isExempt } from "."; +import { ExemptionStore } from "../../operator/controllers/exemptions/exemption-store"; import { MatcherKind, Policy } from "../../operator/crd"; -import { policyExemptionMap } from "../common"; describe("test registering exemptions", () => { beforeAll(() => { - jest.spyOn(policyExemptionMap, "get").mockReturnValue([ + ExemptionStore.init(); + jest.spyOn(ExemptionStore, "getByPolicy").mockReturnValue([ { namespace: "neuvector", name: "^neuvector-enforcer-pod-.*", diff --git a/src/pepr/policies/exemptions/index.ts b/src/pepr/policies/exemptions/index.ts index 9c6708104..8eb2ca73c 100644 --- a/src/pepr/policies/exemptions/index.ts +++ b/src/pepr/policies/exemptions/index.ts @@ -1,7 +1,7 @@ import { KubernetesObject } from "kubernetes-fluent-client"; import { Log, PeprMutateRequest, PeprValidateRequest } from "pepr"; +import { ExemptionStore } from "../../operator/controllers/exemptions/exemption-store"; import { Policy } from "../../operator/crd"; -import { policyExemptionMap } from "../common"; /** * Check a resource against an exemption list for use by the validation action. @@ -14,13 +14,13 @@ export function isExempt( request: PeprValidateRequest | PeprMutateRequest, policy: Policy, ) { - const exemptList = policyExemptionMap.get(policy) || []; + const exemptList = ExemptionStore.getByPolicy(policy); const resourceName = request.Raw.metadata?.name || request.Raw.metadata?.generateName; const resourceNamespace = request.Raw.metadata?.namespace; if (exemptList.length != 0) { // Debug log to provide current exemptions for policy - Log.info( + Log.debug( `Checking ${resourceName} against ${policy} exemptions: ${JSON.stringify(exemptList)}`, ); for (const exempt of exemptList) { diff --git a/src/pepr/policies/index.ts b/src/pepr/policies/index.ts index fe450e7bd..18d9af86c 100644 --- a/src/pepr/policies/index.ts +++ b/src/pepr/policies/index.ts @@ -1,8 +1,8 @@ // Various validation actions for Kubernetes resources from Big Bang import { K8s, Log } from "pepr"; +import { ExemptionStore } from "../operator/controllers/exemptions/exemption-store"; import { processExemptions } from "../operator/controllers/exemptions/exemptions"; import { Matcher, Policy, UDSExemption } from "../operator/crd"; -import { policyExemptionMap } from "./common"; import "./networking"; import "./security"; import "./storage"; @@ -13,18 +13,14 @@ export type StoredMatcher = Matcher & { owner: string }; export type PolicyMap = Map; export async function startExemptionWatch() { - // initialize in-memory map - const policyList = Object.values(Policy); - for (const p of policyList) { - policyExemptionMap.set(p, []); - } + ExemptionStore.init(); // only run in admission controller if (process.env.PEPR_WATCH_MODE === "false") { const watcher = K8s(UDSExemption).Watch(async (exemption, phase) => { Log.debug(`Processing exemption ${exemption.metadata?.name}, watch phase: ${phase}`); - processExemptions(exemption, phase, policyExemptionMap); + processExemptions(exemption, phase); }); // This will run until the process is terminated or the watch is aborted