Files
coder/coderd/rbac/policy.rego
2022-08-11 23:33:50 -05:00

159 lines
4.9 KiB
Rego

package authz
import future.keywords
# A great playground: https://play.openpolicyagent.org/
# Helpful cli commands to debug.
# opa eval --format=pretty 'data.authz.allow = true' -d policy.rego -i input.json
# opa eval --partial --format=pretty 'data.authz.allow = true' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner -i input.json
#
# This policy is specifically constructed to compress to a set of queries if the
# object's 'owner' and 'org_owner' fields are unknown. There is no specific set
# of rules that will guarantee that this policy has this property. However, there
# are some tricks. A unit test will enforce this property, so any edits that pass
# the unit test will be ok.
#
# Tricks: (It's hard to really explain this, fiddling is required)
# 1. Do not use unknown fields in any comprehension or iteration.
# 2. Use the unknown fields as minimally as possible.
# 3. Avoid making code branches based on the value of the unknown field.
# Unknown values are like a "set" of possible values.
# (This is why rule 1 usually breaks things)
# For example:
# In the org section, we calculate the 'allow' number for all orgs, rather
# than just the input.object.org_owner. This is because if the org_owner
# changes, then we don't need to recompute any 'allow' sets. We already have
# the 'allow' for the changed value. So the answer is in a lookup table.
# The final statement 'num := allow[input.object.org_owner]' does not have
# different code branches based on the org_owner. 'num's value does, but
# that is the whole point of partial evaluation.
# bool_flip lets you assign a value to an inverted bool.
# You cannot do 'x := !false', but you can do 'x := bool_flip(false)'
bool_flip(b) = flipped {
b
flipped = false
}
bool_flip(b) = flipped {
not b
flipped = true
}
# number is a quick way to get a set of {true, false} and convert it to
# -1: {false, true} or {false}
# 0: {}
# 1: {true}
number(set) = c {
count(set) == 0
c := 0
}
number(set) = c {
false in set
c := -1
}
number(set) = c {
not false in set
set[_]
c := 1
}
# site, org, and user rules are all similar. Each rule should return a number
# from [-1, 1]. The number corresponds to "negative", "abstain", and "positive"
# for the given level. See the 'allow' rules for how these numbers are used.
default site = 0
site := num {
# allow is a set of boolean values without duplicates.
allow := { x |
# Iterate over all site permissions in all roles
perm := input.subject.roles[_].site[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# x is either 'true' or 'false' if a matching permission exists.
x := bool_flip(perm.negate)
}
num := number(allow)
}
# org_members is the list of organizations the actor is apart of.
org_members := { orgID |
input.subject.roles[_].org[orgID]
}
# org is the same as 'site' except we need to iterate over each organization
# that the actor is a member of.
default org = 0
org := num {
allow := { id: num |
id := org_members[_]
set := { x |
perm := input.subject.roles[_].org[id][_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
x := bool_flip(perm.negate)
}
num := number(set)
}
# Return only the org value of the input's org.
# The reason why we do not do this up front, is that we need to make sure
# this policy compresses down to simple queries. One way to ensure this is
# to keep unknown values out of comprehensions.
# (https://www.openpolicyagent.org/docs/latest/policy-language/#comprehensions)
num := allow[input.object.org_owner]
}
# 'org_mem' is set to true if the user is an org member
org_mem := true {
input.object.org_owner != ""
input.object.org_owner in org_members
}
# If the object has no organization, then the user is also considered part of
# the non-existent org.
org_mem := true {
input.object.org_owner == ""
}
# User is the same as the site, except it only applies if the user owns the object and
# the user is apart of the org (if the object has an org).
default user = 0
user := num {
input.object.owner != ""
input.subject.id = input.object.owner
allow := { x |
perm := input.subject.roles[_].user[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
x := bool_flip(perm.negate)
}
num := number(allow)
}
# The allow block is quite simple. Any set with `-1` cascades down in levels.
# 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.
default allow = false
allow {
site = 1
}
allow {
not site = -1
org = 1
}
allow {
not site = -1
not org = -1
# If we are not a member of an org, and the object has an org, then we are
# not authorized. This is an "implied -1" for not being in the org.
org_mem
user = 1
}