-
Notifications
You must be signed in to change notification settings - Fork 0
/
gatekeeper.rego
381 lines (306 loc) · 9.83 KB
/
gatekeeper.rego
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package hooks.target.library
##################
# Required Hooks #
##################
autoreject_review[rejection] {
constraint := data["constraints"][_][_]
spec := object.get(constraint, "spec", {})
match := object.get(spec, "match", {})
not data["inventory"].cluster["v1"]["Namespace"][input.review.namespace]
not input.review._unstable.namespace
not input.review.namespace == ""
has_field(match, "namespaceSelector")
rejection := {
"msg": "Namespace is not cached in OPA.",
"details": {},
"constraint": constraint,
}
}
matching_constraints[constraint] {
constraint := data["constraints"][_][_]
spec := object.get(constraint, "spec", {})
match := object.get(spec, "match", {})
any_kind_selector_matches(match)
matches_namespaces(match)
does_not_match_excludednamespaces(match)
matches_nsselector(match)
matches_scope(match)
label_selector := object.get(match, "labelSelector", {})
any_labelselector_match(label_selector)
}
# Namespace-scoped objects
matching_reviews_and_constraints[[review, constraint]] {
obj = data["inventory"].namespace[namespace][api_version][kind][name]
r := make_review(obj, api_version, kind, name)
review := add_field(r, "namespace", namespace)
matching_constraints[constraint] with input as {"review": review}
}
# Cluster-scoped objects
matching_reviews_and_constraints[[review, constraint]] {
obj = data["inventory"].cluster[api_version][kind][name]
review = make_review(obj, api_version, kind, name)
matching_constraints[constraint] with input as {"review": review}
}
make_review(obj, api_version, kind, name) = review {
[group, version] := make_group_version(api_version)
review := {
"kind": {"group": group, "version": version, "kind": kind},
"name": name,
"object": obj
}
}
########
# Util #
########
make_group_version(api_version) = [group, version] {
contains(api_version, "/")
[group, version] := split(api_version, "/")
}
make_group_version(api_version) = [group, version] {
not contains(api_version, "/")
group := ""
version := api_version
}
add_field(obj, key, value) = ret {
keys := {k | obj[k]}
allKeys = keys | {key}
ret := {k: v | v = object.get(obj, k, value); allKeys[k]}
}
# has_field returns whether an object has a field
has_field(object, field) = true {
object[field]
}
# False is a tricky special case, as false responses would create an undefined document unless
# they are explicitly tested for
has_field(object, field) = true {
object[field] == false
}
has_field(object, field) = false {
not object[field]
not object[field] == false
}
# get_default returns the value of an object's field or the provided default value.
# It avoids creating an undefined state when trying to access an object attribute that does
# not exist. It considers a null value to be missing.
get_default(object, field, _default) = output {
has_field(object, field)
output = object[field]
output != null
}
get_default(object, field, _default) = output {
has_field(object, field)
object[field] == null
output = _default
}
get_default(object, field, _default) = output {
has_field(object, field) == false
output = _default
}
#######################
# Kind Selector Logic #
#######################
any_kind_selector_matches(match) {
kind_selectors := object.get(match, "kinds", [{"apiGroups": ["*"], "kinds": ["*"]}])
ks := kind_selectors[_]
kind_selector_matches(ks)
}
kind_selector_matches(ks) {
group_matches(ks)
kind_matches(ks)
}
group_matches(ks) {
ks.apiGroups[_] == "*"
}
group_matches(ks) {
ks.apiGroups[_] == input.review.kind.group
}
kind_matches(ks) {
ks.kinds[_] == "*"
}
kind_matches(ks) {
ks.kinds[_] == input.review.kind.kind
}
########################
# Scope Selector Logic #
########################
matches_scope(match) {
not match.scope
}
matches_scope(match) {
match.scope == "*"
}
matches_scope(match) {
match.scope == "Namespaced"
object.get(input.review, "namespace", "") != ""
}
matches_scope(match) {
match.scope == "Cluster"
object.get(input.review, "namespace", "") == ""
}
########################
# Label Selector Logic #
########################
# match_expression_violated checks to see if a match expression is violated.
match_expression_violated("In", labels, key, values) = true {
not labels.key
}
match_expression_violated("In", labels, key, values) = true {
# values array must be non-empty for rule to be valid
count(values) > 0
valueSet := {v | v = values[_]}
count({labels[key]} - valueSet) != 0
}
# No need to check if labels has the key, because a missing key is automatic non-violation
match_expression_violated("NotIn", labels, key, values) = true {
# values array must be non-empty for rule to be valid
count(values) > 0
valueSet := {v | v = values[_]}
count({labels[key]} - valueSet) == 0
}
match_expression_violated("Exists", labels, key, values) = true {
not labels.key
}
match_expression_violated("DoesNotExist", labels, key, values) = true {
labels.key
}
# Checks to see if a kubernetes LabelSelector matches a given set of labels
# A non-existent selector or labels should be represented by an empty object ("{}")
matches_label_selector(selector, labels) {
keys := {key | labels[key]}
matchLabels := object.get(selector, "matchLabels", {})
satisfiedMatchLabels := {key | matchLabels[key] == labels[key]}
count(satisfiedMatchLabels) == count(matchLabels)
matchExpressions := object.get(selector, "matchExpressions", [])
mismatches := {failure | failure = true; failure = match_expression_violated(
matchExpressions[i]["operator"],
labels,
matchExpressions[i]["key"],
object.get(matchExpressions[i], "values", []))}
any(mismatches) == false
}
# object exists, old object is undefined
any_labelselector_match(label_selector) {
object.get(input.review, "oldObject", {}) == {}
object.get(input.review, "object", {}) != {}
obj := object.get(input.review, "object", {})
metadata := object.get(obj, "metadata", {})
labels := object.get(metadata, "labels", {})
matches_label_selector(label_selector, labels)
}
# old object exists, object is undefined
any_labelselector_match(label_selector) {
object.get(input.review, "oldObject", {}) != {}
object.get(input.review, "object", {}) == {}
obj := object.get(input.review, "oldObject", {})
metadata := object.get(obj, "metadata", {})
labels := object.get(metadata, "labels", {})
matches_label_selector(label_selector, labels)
}
# both object and old object are defined
any_labelselector_match(label_selector) {
object.get(input.review, "oldObject", {}) != {}
object.get(input.review, "object", {}) != {}
obj := object.get(input.review, "object", {})
metadata := object.get(obj, "metadata", {})
labels := object.get(metadata, "labels", {})
old_obj := object.get(input.review, "oldObject", {})
old_metadata := object.get(old_obj, "metadata", {})
old_labels := object.get(old_metadata, "labels", {})
all_labels := [labels, old_labels]
matches := {matches | l := all_labels[_]; matches := matches_label_selector(label_selector, l)}
any(matches)
}
# neither object nor old object are defined
# this should never happen, included for completeness
any_labelselector_match(label_selector) {
object.get(input.review, "oldObject", {}) == {}
object.get(input.review, "object", {}) == {}
labels = {}
matches_label_selector(label_selector, labels)
}
############################
# Namespace Selector Logic #
############################
is_ns(kind) {
kind.group == ""
kind.kind == "Namespace"
}
get_ns[out] {
out := input.review._unstable.namespace
}
get_ns[out] {
not input.review._unstable.namespace
out := data["inventory"].cluster["v1"]["Namespace"][input.review.namespace]
}
get_ns_name[out] {
is_ns(input.review.kind)
out := input.review.object.metadata.name
}
get_ns_name[out] {
not is_ns(input.review.kind)
out := input.review.namespace
}
always_match_ns_selectors(match) {
not is_ns(input.review.kind)
object.get(input.review, "namespace", "") == ""
}
matches_namespaces(match) {
not match.namespaces
}
# Always match cluster scoped resources, unless resource is namespace
matches_namespaces(match) {
match.namespaces
always_match_ns_selectors(match)
}
matches_namespaces(match) {
match.namespaces
not always_match_ns_selectors(match)
get_ns_name[ns]
nss := {n | n = match.namespaces[_]}
count({ns} - nss) == 0
}
does_not_match_excludednamespaces(match) {
not match.excludedNamespaces
}
# Always match cluster scoped resources, unless resource is namespace
does_not_match_excludednamespaces(match) {
match.excludedNamespaces
always_match_ns_selectors(match)
}
does_not_match_excludednamespaces(match) {
match.excludedNamespaces
not always_match_ns_selectors(match)
get_ns_name[ns]
nss := {n | n = match.excludedNamespaces[_]}
count({ns} - nss) != 0
}
matches_nsselector(match) {
not match.namespaceSelector
}
# Always match cluster scoped resources, unless resource is namespace
matches_nsselector(match) {
match.namespaceSelector
always_match_ns_selectors(match)
}
matches_nsselector(match) {
not is_ns(input.review.kind)
not always_match_ns_selectors(match)
match.namespaceSelector
get_ns[ns]
matches_namespace_selector(match, ns)
}
# if we are matching against a namespace, match against either the old or new object
matches_nsselector(match) {
is_ns(input.review.kind)
not always_match_ns_selectors(match)
match.namespaceSelector
any_labelselector_match(object.get(match, "namespaceSelector", {}))
}
# Checks to see if a kubernetes NamespaceSelector matches a namespace with a given set of labels
# A non-existent selector or labels should be represented by an empty object ("{}")
matches_namespace_selector(match, ns) {
metadata := object.get(ns, "metadata", {})
nslabels := object.get(metadata, "labels", {})
namespace_selector := object.get(match, "namespaceSelector", {})
matches_label_selector(namespace_selector, nslabels)
}