pepr 0.46.0-nightly.0 → 0.46.0-nightly.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/controller.js +1 -1
- package/dist/lib/assets/defaultTestObjects.d.ts +3 -0
- package/dist/lib/assets/defaultTestObjects.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/admissionRequest.d.ts +9 -0
- package/dist/lib/filter/adjudicators/admissionRequest.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/binding.d.ts +31 -0
- package/dist/lib/filter/adjudicators/binding.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/kubernetesObject.d.ts +22 -0
- package/dist/lib/filter/adjudicators/kubernetesObject.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/mismatch.d.ts +16 -0
- package/dist/lib/filter/adjudicators/mismatch.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/postCollection.d.ts +10 -0
- package/dist/lib/filter/adjudicators/postCollection.d.ts.map +1 -0
- package/dist/lib/filter/filter.d.ts.map +1 -1
- package/dist/lib.js +196 -152
- package/dist/lib.js.map +4 -4
- package/package.json +4 -5
- package/src/lib/assets/defaultTestObjects.ts +533 -0
- package/src/lib/filter/adjudicators/admissionRequest.ts +25 -0
- package/src/lib/filter/adjudicators/binding.ts +85 -0
- package/src/lib/filter/adjudicators/kubernetesObject.ts +90 -0
- package/src/lib/filter/adjudicators/mismatch.ts +129 -0
- package/src/lib/filter/adjudicators/postCollection.ts +77 -0
- package/src/lib/filter/filter.ts +22 -24
- package/src/lib/processors/watch-processor.ts +1 -1
- package/dist/lib/filter/adjudicators/adjudicators.d.ts +0 -74
- package/dist/lib/filter/adjudicators/adjudicators.d.ts.map +0 -1
- package/src/lib/filter/adjudicators/adjudicators.ts +0 -334
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"!src/**/*.test.ts",
|
|
16
16
|
"!dist/**/*.test.d.ts*"
|
|
17
17
|
],
|
|
18
|
-
"version": "0.46.0-nightly.
|
|
18
|
+
"version": "0.46.0-nightly.10",
|
|
19
19
|
"main": "dist/lib.js",
|
|
20
20
|
"types": "dist/lib.d.ts",
|
|
21
21
|
"scripts": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"follow-redirects": "1.15.9",
|
|
53
53
|
"http-status-codes": "^2.3.0",
|
|
54
54
|
"json-pointer": "^0.6.2",
|
|
55
|
-
"kubernetes-fluent-client": "3.4.
|
|
55
|
+
"kubernetes-fluent-client": "3.4.2",
|
|
56
56
|
"pino": "9.6.0",
|
|
57
57
|
"pino-pretty": "13.0.0",
|
|
58
58
|
"prom-client": "15.1.3",
|
|
@@ -76,12 +76,11 @@
|
|
|
76
76
|
"jest": "29.7.0",
|
|
77
77
|
"js-yaml": "^4.1.0",
|
|
78
78
|
"shellcheck": "^3.0.0",
|
|
79
|
-
"ts-jest": "29.2.
|
|
79
|
+
"ts-jest": "29.2.6",
|
|
80
80
|
"undici": "^7.0.1"
|
|
81
81
|
},
|
|
82
82
|
"overrides": {
|
|
83
|
-
"glob": "^9.0.0"
|
|
84
|
-
"jsonpath-plus": "^10.3.0"
|
|
83
|
+
"glob": "^9.0.0"
|
|
85
84
|
},
|
|
86
85
|
"peerDependencies": {
|
|
87
86
|
"@types/prompts": "2.4.9",
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { GenericClass } from "kubernetes-fluent-client";
|
|
2
|
+
import { Event } from "../enums";
|
|
3
|
+
import { CapabilityExport } from "../types";
|
|
4
|
+
import { describe, beforeEach, jest, it, expect } from "@jest/globals";
|
|
5
|
+
import { V1PolicyRule as PolicyRule } from "@kubernetes/client-node";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { clusterRole } from "./rbac";
|
|
8
|
+
import * as helpers from "../helpers";
|
|
9
|
+
|
|
10
|
+
export const mockCapabilities: CapabilityExport[] = [
|
|
11
|
+
{
|
|
12
|
+
rbac: [
|
|
13
|
+
{
|
|
14
|
+
apiGroups: ["pepr.dev"],
|
|
15
|
+
resources: ["peprstores"],
|
|
16
|
+
verbs: ["create", "get", "patch", "watch"],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
bindings: [
|
|
20
|
+
{
|
|
21
|
+
kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" },
|
|
22
|
+
isWatch: false,
|
|
23
|
+
event: Event.CREATE,
|
|
24
|
+
model: {} as GenericClass,
|
|
25
|
+
filters: {
|
|
26
|
+
name: "",
|
|
27
|
+
regexName: "",
|
|
28
|
+
namespaces: [],
|
|
29
|
+
regexNamespaces: [],
|
|
30
|
+
labels: {},
|
|
31
|
+
annotations: {},
|
|
32
|
+
deletionTimestamp: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
hasSchedule: false,
|
|
37
|
+
name: "",
|
|
38
|
+
description: "",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
rbac: [
|
|
42
|
+
{
|
|
43
|
+
apiGroups: ["apiextensions.k8s.io"],
|
|
44
|
+
resources: ["customresourcedefinitions"],
|
|
45
|
+
verbs: ["patch", "create"],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
bindings: [
|
|
49
|
+
{
|
|
50
|
+
kind: {
|
|
51
|
+
group: "apiextensions.k8s.io",
|
|
52
|
+
version: "v1",
|
|
53
|
+
kind: "customresourcedefinition",
|
|
54
|
+
plural: "customresourcedefinitions",
|
|
55
|
+
},
|
|
56
|
+
isWatch: false,
|
|
57
|
+
isFinalize: false,
|
|
58
|
+
event: Event.CREATE,
|
|
59
|
+
model: {} as GenericClass,
|
|
60
|
+
filters: {
|
|
61
|
+
name: "",
|
|
62
|
+
regexName: "",
|
|
63
|
+
namespaces: [],
|
|
64
|
+
regexNamespaces: [],
|
|
65
|
+
labels: {},
|
|
66
|
+
annotations: {},
|
|
67
|
+
deletionTimestamp: false,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
hasSchedule: false,
|
|
72
|
+
name: "",
|
|
73
|
+
description: "",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
rbac: [
|
|
77
|
+
{
|
|
78
|
+
apiGroups: [""],
|
|
79
|
+
resources: ["namespaces"],
|
|
80
|
+
verbs: ["watch"],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
bindings: [
|
|
84
|
+
{
|
|
85
|
+
kind: { group: "", version: "v1", kind: "namespace", plural: "namespaces" },
|
|
86
|
+
isWatch: true,
|
|
87
|
+
isFinalize: false,
|
|
88
|
+
event: Event.CREATE,
|
|
89
|
+
model: {} as GenericClass,
|
|
90
|
+
filters: {
|
|
91
|
+
name: "",
|
|
92
|
+
regexName: "",
|
|
93
|
+
namespaces: [],
|
|
94
|
+
regexNamespaces: [],
|
|
95
|
+
labels: {},
|
|
96
|
+
annotations: {},
|
|
97
|
+
deletionTimestamp: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
hasSchedule: false,
|
|
102
|
+
name: "",
|
|
103
|
+
description: "",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
rbac: [
|
|
107
|
+
{
|
|
108
|
+
apiGroups: [""],
|
|
109
|
+
resources: ["configmaps"],
|
|
110
|
+
verbs: ["watch"],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
bindings: [
|
|
114
|
+
{
|
|
115
|
+
kind: { group: "", version: "v1", kind: "configmap", plural: "configmaps" },
|
|
116
|
+
isWatch: true,
|
|
117
|
+
isFinalize: false,
|
|
118
|
+
event: Event.CREATE,
|
|
119
|
+
model: {} as GenericClass,
|
|
120
|
+
filters: {
|
|
121
|
+
name: "",
|
|
122
|
+
regexName: "",
|
|
123
|
+
namespaces: [],
|
|
124
|
+
regexNamespaces: [],
|
|
125
|
+
labels: {},
|
|
126
|
+
annotations: {},
|
|
127
|
+
deletionTimestamp: false,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
hasSchedule: false,
|
|
132
|
+
name: "",
|
|
133
|
+
description: "",
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
describe("RBAC generation", () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
jest.clearAllMocks();
|
|
139
|
+
const mockPackageJsonRBAC = {};
|
|
140
|
+
|
|
141
|
+
jest.spyOn(fs, "readFileSync").mockImplementation((path: unknown) => {
|
|
142
|
+
if (typeof path === "string" && path.includes("package.json")) {
|
|
143
|
+
return JSON.stringify({ rbac: mockPackageJsonRBAC });
|
|
144
|
+
}
|
|
145
|
+
return "{}";
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should generate correct ClusterRole rules in scoped mode", () => {
|
|
150
|
+
const result = clusterRole("test-role", mockCapabilities, "scoped", []);
|
|
151
|
+
|
|
152
|
+
expect(result.rules).toEqual([
|
|
153
|
+
{
|
|
154
|
+
apiGroups: ["pepr.dev"],
|
|
155
|
+
resources: ["peprstores"],
|
|
156
|
+
verbs: ["create", "get", "patch", "watch"],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
apiGroups: ["apiextensions.k8s.io"],
|
|
160
|
+
resources: ["customresourcedefinitions"],
|
|
161
|
+
verbs: ["patch", "create"],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
apiGroups: [""],
|
|
165
|
+
resources: ["namespaces"],
|
|
166
|
+
verbs: ["watch"],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
apiGroups: [""],
|
|
170
|
+
resources: ["configmaps"],
|
|
171
|
+
verbs: ["watch"],
|
|
172
|
+
},
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should generate a ClusterRole with wildcard rules when not in scoped mode", () => {
|
|
177
|
+
const expectedWildcardRules = [
|
|
178
|
+
{
|
|
179
|
+
apiGroups: ["*"],
|
|
180
|
+
resources: ["*"],
|
|
181
|
+
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const result = clusterRole("test-role", mockCapabilities, "admin", []);
|
|
186
|
+
|
|
187
|
+
expect(result.rules).toEqual(expectedWildcardRules);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should return an empty rules array when capabilities are empty in scoped mode", () => {
|
|
191
|
+
const result = clusterRole("test-role", [], "scoped", []);
|
|
192
|
+
|
|
193
|
+
expect(result.rules).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should include finalize verbs if isFinalize is true in scoped mode", () => {
|
|
197
|
+
const capabilitiesWithFinalize: CapabilityExport[] = [
|
|
198
|
+
{
|
|
199
|
+
rbac: [
|
|
200
|
+
{
|
|
201
|
+
apiGroups: ["pepr.dev"],
|
|
202
|
+
resources: ["peprstores"],
|
|
203
|
+
verbs: ["patch"],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
bindings: [
|
|
207
|
+
{
|
|
208
|
+
kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" },
|
|
209
|
+
isWatch: false,
|
|
210
|
+
isFinalize: true,
|
|
211
|
+
event: Event.CREATE,
|
|
212
|
+
model: {} as GenericClass,
|
|
213
|
+
filters: {
|
|
214
|
+
name: "",
|
|
215
|
+
regexName: "",
|
|
216
|
+
namespaces: [],
|
|
217
|
+
regexNamespaces: [],
|
|
218
|
+
labels: {},
|
|
219
|
+
annotations: {},
|
|
220
|
+
deletionTimestamp: false,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
hasSchedule: false,
|
|
225
|
+
name: "",
|
|
226
|
+
description: "",
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const result = clusterRole(
|
|
231
|
+
"test-role",
|
|
232
|
+
capabilitiesWithFinalize,
|
|
233
|
+
"scoped",
|
|
234
|
+
capabilitiesWithFinalize.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(result.rules).toEqual([
|
|
238
|
+
{
|
|
239
|
+
apiGroups: ["pepr.dev"],
|
|
240
|
+
resources: ["peprstores"],
|
|
241
|
+
verbs: ["patch"],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
apiGroups: ["apiextensions.k8s.io"],
|
|
245
|
+
resources: ["customresourcedefinitions"],
|
|
246
|
+
verbs: ["patch", "create"],
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should deduplicate verbs and resources in rules", () => {
|
|
252
|
+
const capabilitiesWithDuplicates: CapabilityExport[] = [
|
|
253
|
+
{
|
|
254
|
+
rbac: [
|
|
255
|
+
{
|
|
256
|
+
apiGroups: ["pepr.dev"],
|
|
257
|
+
resources: ["peprstores"],
|
|
258
|
+
verbs: ["create", "get"],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
bindings: [
|
|
262
|
+
{
|
|
263
|
+
kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" },
|
|
264
|
+
isWatch: false,
|
|
265
|
+
event: Event.CREATE,
|
|
266
|
+
model: {} as GenericClass,
|
|
267
|
+
filters: {
|
|
268
|
+
name: "",
|
|
269
|
+
regexName: "",
|
|
270
|
+
namespaces: [],
|
|
271
|
+
regexNamespaces: [],
|
|
272
|
+
labels: {},
|
|
273
|
+
annotations: {},
|
|
274
|
+
deletionTimestamp: false,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
hasSchedule: false,
|
|
279
|
+
name: "",
|
|
280
|
+
description: "",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
rbac: [
|
|
284
|
+
{
|
|
285
|
+
apiGroups: ["pepr.dev"],
|
|
286
|
+
resources: ["peprstores"],
|
|
287
|
+
verbs: ["get", "patch"],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
bindings: [
|
|
291
|
+
{
|
|
292
|
+
kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" },
|
|
293
|
+
isWatch: false,
|
|
294
|
+
event: Event.CREATE,
|
|
295
|
+
model: {} as GenericClass,
|
|
296
|
+
filters: {
|
|
297
|
+
name: "",
|
|
298
|
+
regexName: "",
|
|
299
|
+
namespaces: [],
|
|
300
|
+
regexNamespaces: [],
|
|
301
|
+
labels: {},
|
|
302
|
+
annotations: {},
|
|
303
|
+
deletionTimestamp: false,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
hasSchedule: false,
|
|
308
|
+
name: "",
|
|
309
|
+
description: "",
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const result = clusterRole(
|
|
314
|
+
"test-role",
|
|
315
|
+
capabilitiesWithDuplicates,
|
|
316
|
+
"scoped",
|
|
317
|
+
capabilitiesWithDuplicates.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Filter out only the rules for 'pepr.dev' and 'peprstores'
|
|
321
|
+
const filteredRules = result.rules?.filter(
|
|
322
|
+
rule => rule.apiGroups?.includes("pepr.dev") && rule.resources?.includes("peprstores"),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(filteredRules).toEqual([
|
|
326
|
+
{
|
|
327
|
+
apiGroups: ["pepr.dev"],
|
|
328
|
+
resources: ["peprstores"],
|
|
329
|
+
verbs: ["create", "get", "patch", "watch"],
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe("clusterRole", () => {
|
|
335
|
+
// Mocking the readRBACFromPackageJson function to return null
|
|
336
|
+
jest.mock("./rbac", () => ({
|
|
337
|
+
...(jest.requireActual("./rbac") as object),
|
|
338
|
+
readRBACFromPackageJson: jest.fn(() => null),
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// Mocking createRBACMap to isolate the behavior of clusterRole function
|
|
342
|
+
jest.mock("../helpers", () => ({
|
|
343
|
+
...(jest.requireActual("../helpers") as object),
|
|
344
|
+
createRBACMap: jest.fn(),
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
beforeEach(() => {
|
|
348
|
+
jest.clearAllMocks();
|
|
349
|
+
jest.restoreAllMocks();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should handle keys with less than 3 segments and set group to an empty string", () => {
|
|
353
|
+
jest.spyOn(helpers, "createRBACMap").mockReturnValue({
|
|
354
|
+
nodes: {
|
|
355
|
+
plural: "nodes",
|
|
356
|
+
verbs: ["get"],
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const capabilitiesWithShortKey: CapabilityExport[] = [
|
|
361
|
+
{
|
|
362
|
+
rbac: [
|
|
363
|
+
{
|
|
364
|
+
apiGroups: [""],
|
|
365
|
+
resources: ["nodes"],
|
|
366
|
+
verbs: ["get"],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
bindings: [
|
|
370
|
+
{
|
|
371
|
+
kind: { group: "", version: "v1", kind: "node", plural: "nodes" },
|
|
372
|
+
isWatch: false,
|
|
373
|
+
event: Event.CREATE,
|
|
374
|
+
model: {} as GenericClass,
|
|
375
|
+
filters: {
|
|
376
|
+
name: "",
|
|
377
|
+
regexName: "",
|
|
378
|
+
namespaces: [],
|
|
379
|
+
regexNamespaces: [],
|
|
380
|
+
labels: {},
|
|
381
|
+
annotations: {},
|
|
382
|
+
deletionTimestamp: false,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
hasSchedule: false,
|
|
387
|
+
name: "",
|
|
388
|
+
description: "",
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const result = clusterRole(
|
|
393
|
+
"test-role",
|
|
394
|
+
capabilitiesWithShortKey,
|
|
395
|
+
"scoped",
|
|
396
|
+
capabilitiesWithShortKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(result.rules).toEqual([
|
|
400
|
+
{
|
|
401
|
+
apiGroups: [""],
|
|
402
|
+
resources: ["nodes"],
|
|
403
|
+
verbs: ["get"],
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should handle keys with 3 or more segments and set group correctly", () => {
|
|
409
|
+
jest.spyOn(helpers, "createRBACMap").mockReturnValue({
|
|
410
|
+
"apps/v1/deployments": {
|
|
411
|
+
plural: "deployments",
|
|
412
|
+
verbs: ["create"],
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const capabilitiesWithLongKey: CapabilityExport[] = [
|
|
417
|
+
{
|
|
418
|
+
rbac: [
|
|
419
|
+
{
|
|
420
|
+
apiGroups: ["apps"],
|
|
421
|
+
resources: ["deployments"],
|
|
422
|
+
verbs: ["create"],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
bindings: [
|
|
426
|
+
{
|
|
427
|
+
kind: { group: "apps", version: "v1", kind: "deployment", plural: "deployments" },
|
|
428
|
+
isWatch: false,
|
|
429
|
+
event: Event.CREATE,
|
|
430
|
+
model: {} as GenericClass,
|
|
431
|
+
filters: {
|
|
432
|
+
name: "",
|
|
433
|
+
regexName: "",
|
|
434
|
+
namespaces: [],
|
|
435
|
+
regexNamespaces: [],
|
|
436
|
+
labels: {},
|
|
437
|
+
annotations: {},
|
|
438
|
+
deletionTimestamp: false,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
hasSchedule: false,
|
|
443
|
+
name: "",
|
|
444
|
+
description: "",
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const result = clusterRole(
|
|
449
|
+
"test-role",
|
|
450
|
+
capabilitiesWithLongKey,
|
|
451
|
+
"scoped",
|
|
452
|
+
capabilitiesWithLongKey.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
expect(result.rules).toEqual([
|
|
456
|
+
{
|
|
457
|
+
apiGroups: ["apps"],
|
|
458
|
+
resources: ["deployments"],
|
|
459
|
+
verbs: ["create"],
|
|
460
|
+
},
|
|
461
|
+
]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should handle non-array custom RBAC by defaulting to an empty array", () => {
|
|
465
|
+
// Mock readRBACFromPackageJson to return a non-array value
|
|
466
|
+
jest.spyOn(fs, "readFileSync").mockImplementation(() => {
|
|
467
|
+
return JSON.stringify({
|
|
468
|
+
pepr: {
|
|
469
|
+
rbac: "not-an-array", // Simulate invalid RBAC structure
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const result = clusterRole(
|
|
475
|
+
"test-role",
|
|
476
|
+
mockCapabilities,
|
|
477
|
+
"scoped",
|
|
478
|
+
mockCapabilities.flatMap(c => c.rbac).filter((rule): rule is PolicyRule => rule !== undefined),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// The result should only contain rules from the capabilities, not from the invalid custom RBAC
|
|
482
|
+
expect(result.rules).toEqual([
|
|
483
|
+
{
|
|
484
|
+
apiGroups: ["pepr.dev"],
|
|
485
|
+
resources: ["peprstores"],
|
|
486
|
+
verbs: ["create", "get", "patch", "watch"],
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
apiGroups: ["apiextensions.k8s.io"],
|
|
490
|
+
resources: ["customresourcedefinitions"],
|
|
491
|
+
verbs: ["patch", "create"],
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
apiGroups: [""],
|
|
495
|
+
resources: ["namespaces"],
|
|
496
|
+
verbs: ["watch"],
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
apiGroups: [""],
|
|
500
|
+
resources: ["configmaps"],
|
|
501
|
+
verbs: ["watch"],
|
|
502
|
+
},
|
|
503
|
+
]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should default to an empty verbs array if rule.verbs is undefined", () => {
|
|
507
|
+
// Simulate a custom RBAC rule with empty verbs
|
|
508
|
+
const customRbacWithNoVerbs: PolicyRule[] = [
|
|
509
|
+
{
|
|
510
|
+
apiGroups: ["pepr.dev"],
|
|
511
|
+
resources: ["customresources"],
|
|
512
|
+
verbs: [], // Set verbs to an empty array to satisfy the V1PolicyRule type
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
|
|
516
|
+
jest.spyOn(fs, "readFileSync").mockImplementation(() => {
|
|
517
|
+
return JSON.stringify({
|
|
518
|
+
pepr: {
|
|
519
|
+
rbac: customRbacWithNoVerbs,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const result = clusterRole("test-role", mockCapabilities, "scoped", customRbacWithNoVerbs);
|
|
525
|
+
|
|
526
|
+
// Check that the verbs array is empty for the custom RBAC rule
|
|
527
|
+
expect(result.rules).toContainEqual({
|
|
528
|
+
apiGroups: ["pepr.dev"],
|
|
529
|
+
resources: ["customresources"],
|
|
530
|
+
verbs: [],
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { Operation } from "../../enums";
|
|
5
|
+
import { AdmissionRequest } from "../../types";
|
|
6
|
+
import { defaultTo, pipe } from "ramda";
|
|
7
|
+
import { KubernetesObject } from "kubernetes-fluent-client";
|
|
8
|
+
|
|
9
|
+
export const declaredOperation = pipe(
|
|
10
|
+
(request: AdmissionRequest<KubernetesObject>): Operation => request?.operation,
|
|
11
|
+
defaultTo(""),
|
|
12
|
+
);
|
|
13
|
+
export const declaredGroup = pipe(
|
|
14
|
+
(request: AdmissionRequest<KubernetesObject>): string => request?.kind?.group,
|
|
15
|
+
defaultTo(""),
|
|
16
|
+
);
|
|
17
|
+
export const declaredVersion = pipe(
|
|
18
|
+
(request: AdmissionRequest<KubernetesObject>): string | undefined => request?.kind?.version,
|
|
19
|
+
defaultTo(""),
|
|
20
|
+
);
|
|
21
|
+
export const declaredKind = pipe(
|
|
22
|
+
(request: AdmissionRequest<KubernetesObject>): string => request?.kind?.kind,
|
|
23
|
+
defaultTo(""),
|
|
24
|
+
);
|
|
25
|
+
export const declaredUid = pipe((request: AdmissionRequest<KubernetesObject>): string => request?.uid, defaultTo(""));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { Event } from "../../enums";
|
|
5
|
+
import { Binding, FinalizeAction, WatchLogAction, MutateAction, ValidateAction } from "../../types";
|
|
6
|
+
import { complement, defaultTo, equals, not, pipe } from "ramda";
|
|
7
|
+
import { GenericClass } from "kubernetes-fluent-client";
|
|
8
|
+
|
|
9
|
+
export const definesDeletionTimestamp = pipe(
|
|
10
|
+
(binding: Binding): boolean => binding?.filters?.deletionTimestamp ?? false,
|
|
11
|
+
defaultTo(false),
|
|
12
|
+
);
|
|
13
|
+
export const ignoresDeletionTimestamp = complement(definesDeletionTimestamp);
|
|
14
|
+
|
|
15
|
+
export const definedName = pipe((binding: Binding): string => {
|
|
16
|
+
return binding.filters.name;
|
|
17
|
+
}, defaultTo(""));
|
|
18
|
+
export const definesName = pipe(definedName, equals(""), not);
|
|
19
|
+
export const ignoresName = complement(definesName);
|
|
20
|
+
|
|
21
|
+
export const definedNameRegex = pipe(
|
|
22
|
+
(binding: Partial<Binding>): string | undefined => binding.filters?.regexName,
|
|
23
|
+
defaultTo(""),
|
|
24
|
+
);
|
|
25
|
+
export const definesNameRegex = pipe(definedNameRegex, equals(""), not);
|
|
26
|
+
|
|
27
|
+
export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([]));
|
|
28
|
+
export const definesNamespaces = pipe(definedNamespaces, equals([]), not);
|
|
29
|
+
|
|
30
|
+
export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([]));
|
|
31
|
+
export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not);
|
|
32
|
+
|
|
33
|
+
export const definedAnnotations = pipe((binding: Partial<Binding>) => binding?.filters?.annotations, defaultTo({}));
|
|
34
|
+
export const definesAnnotations = pipe(definedAnnotations, equals({}), not);
|
|
35
|
+
|
|
36
|
+
export const definedLabels = pipe((binding: Partial<Binding>) => binding?.filters?.labels, defaultTo({}));
|
|
37
|
+
export const definesLabels = pipe(definedLabels, equals({}), not);
|
|
38
|
+
|
|
39
|
+
export const definedEvent = (binding: Binding): Event => {
|
|
40
|
+
return binding.event;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const definesDelete = pipe(definedEvent, equals(Event.DELETE));
|
|
44
|
+
|
|
45
|
+
export const definedGroup = pipe((binding): string => binding?.kind?.group, defaultTo(""));
|
|
46
|
+
export const definesGroup = pipe(definedGroup, equals(""), not);
|
|
47
|
+
|
|
48
|
+
export const definedVersion = pipe(
|
|
49
|
+
(binding: Partial<Binding>): string | undefined => binding?.kind?.version,
|
|
50
|
+
defaultTo(""),
|
|
51
|
+
);
|
|
52
|
+
export const definesVersion = pipe(definedVersion, equals(""), not);
|
|
53
|
+
|
|
54
|
+
export const definedKind = pipe((binding): string => binding?.kind?.kind, defaultTo(""));
|
|
55
|
+
export const definesKind = pipe(definedKind, equals(""), not);
|
|
56
|
+
|
|
57
|
+
export const definedCategory = (binding: Partial<Binding>): string => {
|
|
58
|
+
// Ordering matters, finalize is a "watch"
|
|
59
|
+
const categories: { [key: string]: boolean | undefined } = {
|
|
60
|
+
Finalize: binding.isFinalize,
|
|
61
|
+
Watch: binding.isWatch,
|
|
62
|
+
Mutate: binding.isMutate,
|
|
63
|
+
Validate: binding.isValidate,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return Object.keys(categories).find(key => categories[key]) || "";
|
|
67
|
+
};
|
|
68
|
+
export type DefinedCallbackReturnType =
|
|
69
|
+
| FinalizeAction<GenericClass, InstanceType<GenericClass>>
|
|
70
|
+
| WatchLogAction<GenericClass, InstanceType<GenericClass>>
|
|
71
|
+
| MutateAction<GenericClass, InstanceType<GenericClass>>
|
|
72
|
+
| ValidateAction<GenericClass, InstanceType<GenericClass>>
|
|
73
|
+
| null
|
|
74
|
+
| undefined;
|
|
75
|
+
|
|
76
|
+
export const definedCallback = (binding: Partial<Binding>): DefinedCallbackReturnType => {
|
|
77
|
+
// Ordering matters, finalize is a "watch"
|
|
78
|
+
// prettier-ignore
|
|
79
|
+
return binding.isFinalize ? binding.finalizeCallback :
|
|
80
|
+
binding.isWatch ? binding.watchCallback :
|
|
81
|
+
binding.isMutate ? binding.mutateCallback :
|
|
82
|
+
binding.isValidate ? binding.validateCallback :
|
|
83
|
+
null;
|
|
84
|
+
};
|
|
85
|
+
export const definedCallbackName = pipe(definedCallback, defaultTo({ name: "" }), callback => callback.name);
|