forked from coder/coder
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpolicy.rego
More file actions
427 lines (340 loc) · 14.1 KB
/
Copy pathpolicy.rego
File metadata and controls
427 lines (340 loc) · 14.1 KB
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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package authz
import rego.v1
# Check the POLICY.md file before editing this!
#
# https://play.openpolicyagent.org/
#
#==============================================================================#
# Site level rules #
#==============================================================================#
# Site level permissions allow the subject to use that permission on any object.
# For example, a site-level workspace.read permission means that the subject can
# see every workspace in the deployment, regardless of organization or owner.
default site := 0
site := check_site_permissions(input.subject.roles)
default scope_site := 0
scope_site := check_site_permissions([input.subject.scope])
check_site_permissions(roles) := vote if {
allow := {is_allowed |
# Iterate over all site permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].site[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
vote := to_vote(allow)
}
#==============================================================================#
# User level rules #
#==============================================================================#
# User level rules apply to all objects owned by the subject which are not also
# owned by an org. Permissions for objects which are "jointly" owned by an org
# instead defer to the org member level rules.
default user := 0
user := check_user_permissions(input.subject.roles)
default scope_user := 0
scope_user := check_user_permissions([input.subject.scope])
check_user_permissions(roles) := vote if {
# The object must be owned by the subject.
input.subject.id = input.object.owner
# If there is an org, use org_member permissions instead
input.object.org_owner == ""
not input.object.any_org
allow := {is_allowed |
# Iterate over all user permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].user[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
vote := to_vote(allow)
}
#==============================================================================#
# Org level rules #
#==============================================================================#
# Org level permissions are similar to `site`, except we need to iterate over
# each organization that the subject is a member of, and check against the
# organization that the object belongs to.
# For example, an organization-level workspace.read permission means that the
# subject can see every workspace in the organization, regardless of owner.
# org_memberships is the set of organizations the subject is apart of.
org_memberships := {org_id |
input.subject.roles[_].by_org_id[org_id]
}
# TODO: Should there be a scope_org_memberships too? Without it, the membership
# is determined by the user's roles, not their scope permissions.
#
# If an owner (who is not an org member) has an org scope, that org scope will
# fail to return '1', since we assume all non-members return '-1' for org level
# permissions. Adding a second set of org memberships might affect the partial
# evaluation. This is being left until org scopes are used.
default org := 0
org := check_org_permissions(input.subject.roles, "org")
default scope_org := 0
scope_org := check_org_permissions([input.subject.scope], "org")
# check_all_org_permissions creates a map from org ids to votes at each org
# level, for each org that the subject is a member of. It doesn't actually check
# if the object is in the same org. Instead we look up the correct vote from
# this map based on the object's org id in `check_org_permissions`.
# For example, the `org_map` will look something like this:
#
# {"<org_id_a>": 1, "<org_id_b>": 0, "<org_id_c>": -1}
#
# The caller then uses `output[input.object.org_owner]` to get the correct vote.
#
# We have to create this map, rather than just getting the vote of the object's
# org id because the org id _might_ be unknown. In order to make sure that this
# policy compresses down to simple queries we need to keep unknown values out of
# comprehensions.
check_all_org_permissions(roles, key) := {org_id: vote |
org_id := org_memberships[_]
allow := {is_allowed |
# Iterate over all site permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].by_org_id[org_id][key][_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
vote := to_vote(allow)
}
# This check handles the case where the org id is known.
check_org_permissions(roles, key) := vote if {
# Disallow setting any_org at the same time as an org id.
not input.object.any_org
allow_map := check_all_org_permissions(roles, key)
# Return only the vote of the object's org.
vote := allow_map[input.object.org_owner]
}
# This check handles the case where we want to know if the user has the
# appropriate permission for any organization, without needing to know which.
# This is used in several places in the UI to determine if certain parts of the
# app should be accessible.
# For example, can the user create a new template in any organization? If yes,
# then we should show the "New template" button.
check_org_permissions(roles, key) := vote if {
# Require `any_org` to be set
input.object.any_org
allow_map := check_all_org_permissions(roles, key)
# Since we're checking if the subject has the permission in _any_ org, we're
# essentially trying to find the highest vote from any org.
vote := max({vote |
some vote in allow_map
})
}
# is_org_member checks if the subject belong to the same organization as the
# object.
is_org_member if {
not input.object.any_org
input.object.org_owner != ""
input.object.org_owner in org_memberships
}
# ...if 'any_org' is set to true, we check if the subject is a member of any
# org.
is_org_member if {
input.object.any_org
count(org_memberships) > 0
}
#==============================================================================#
# Org member level rules #
#==============================================================================#
# Org member level permissions apply to all objects owned by the subject _and_
# the corresponding org. Permissions for objects which are not owned by an
# organization instead defer to the user level rules.
#
# The rules for this level are very similar to the rules for the organization
# level, and so we reuse the `check_org_permissions` function from those rules.
default org_member := 0
org_member := vote if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
vote := check_org_permissions(input.subject.roles, "member")
}
default scope_org_member := 0
scope_org_member := vote if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
vote := check_org_permissions([input.subject.scope], "member")
}
#==============================================================================#
# Role rules #
#==============================================================================#
# role_allow specifies all of the conditions under which a role can grant
# permission. These rules intentionally use the "unification" operator rather
# than the equality and inequality operators, because those operators do not
# work on partial values.
# https://www.openpolicyagent.org/docs/policy-language#unification-
# Site level authorization
role_allow if {
site = 1
}
# User level authorization
role_allow if {
not site = -1
user = 1
}
# Org level authorization
role_allow if {
not site = -1
org = 1
}
# Org member authorization
role_allow if {
not site = -1
not org = -1
org_member = 1
}
#==============================================================================#
# Scope rules #
#==============================================================================#
# scope_allow specifies all of the conditions under which a scope can grant
# permission. These rules intentionally use the "unification" (=) operator
# rather than the equality (==) and inequality (!=) operators, because those
# operators do not work on partial values.
# https://www.openpolicyagent.org/docs/policy-language#unification-
# Site level scope enforcement
scope_allow if {
object_is_included_in_scope_allow_list
scope_site = 1
}
# User level scope enforcement
scope_allow if {
# User scope permissions must be allowed by the scope, and not denied
# by the site. The object *must not* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
scope_user = 1
}
# Org level scope enforcement
scope_allow if {
# Org member scope permissions must be allowed by the scope, and not denied
# by the site. The object *must* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
scope_org = 1
}
# Org member level scope enforcement
scope_allow if {
# Org member scope permissions must be allowed by the scope, and not denied
# by the site or org. The object *must* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
not scope_org = -1
scope_org_member = 1
}
# If *.* is allowed, then all objects are in scope.
object_is_included_in_scope_allow_list if {
{"type": "*", "id": "*"} in input.subject.scope.allow_list
}
# If <type>.* is allowed, then all objects of that type are in scope.
object_is_included_in_scope_allow_list if {
{"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
}
# Check if the object type and ID match one of the allow list entries.
object_is_included_in_scope_allow_list if {
# Check that the wildcard rules do not apply. This prevents partial inputs
# from needing to include `input.object.id`.
not {"type": "*", "id": "*"} in input.subject.scope.allow_list
not {"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
# Check which IDs from the allow list match the object type
allowed_ids_for_object_type := {it.id |
some it in input.subject.scope.allow_list
it.type in [input.object.type, "*"]
}
# Check if the input object ID is in the set of allowed IDs for the same
# object type. We do this at the end to keep `input.object.id` out of the
# comprehension because it might be unknown.
input.object.id in allowed_ids_for_object_type
}
#==============================================================================#
# ACL rules #
#==============================================================================#
# ACL for users
acl_allow if {
# TODO: Should you have to be a member of the org too?
perms := input.object.acl_user_list[input.subject.id]
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
# ACL for groups
acl_allow if {
# If there is no organization owner, the object cannot be owned by an
# org-scoped group.
is_org_member
some group in input.subject.groups
perms := input.object.acl_group_list[group]
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
# ACL for the special "Everyone" groups
acl_allow if {
# If there is no organization owner, the object cannot be owned by an
# org-scoped group.
is_org_member
perms := input.object.acl_group_list[input.object.org_owner]
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
#==============================================================================#
# Allow #
#==============================================================================#
# The `allow` block is quite simple. Any check that voted no will cascade down.
# Authorization looks for any `allow` statement that is true. Multiple can be
# true! Note that the absence of `allow` means "unauthorized". An explicit
# `"allow": true` is required.
#
# We check both the subject's permissions (given by their roles or by ACL) and
# the subject's scope. (The default scope is "*:*", allowing all actions.) Both
# a permission check (either from roles or ACL) and the scope check must vote to
# allow or the action is not authorized.
# A subject can be given permission by a role
permission_allow if role_allow
# A subject can be given permission by ACL
permission_allow if acl_allow
allow if {
# Must be allowed by the subject's permissions
permission_allow
# ...and allowed by the scope
scope_allow
}
#==============================================================================#
# Utilities #
#==============================================================================#
# bool_flip returns the logical negation of a boolean value. You can't do
# 'x := not false', but you can do 'x := bool_flip(false)'
bool_flip(b) := false if {
b
}
bool_flip(b) if {
not b
}
# to_vote gives you a voting value from a set or list of booleans.
# {false,..} => deny (-1)
# {} => abstain (0)
# {true} => allow (1)
# Any set which contains a `false` should be considered a vote to deny.
to_vote(set) := -1 if {
false in set
}
# A set which is empty should be considered abstaining.
to_vote(set) := 0 if {
count(set) == 0
}
# A set which only contains true should be considered a vote to allow.
to_vote(set) := 1 if {
not false in set
true in set
}