Skip to content

chore: Update rego to be partial execution friendly #3449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Aug 11, 2022
Merged
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore: Update rego to be partial execution friendly
  • Loading branch information
Emyrk committed Aug 10, 2022
commit b3536bc315c9527a1109f48e211fee69b772d4fc
171 changes: 72 additions & 99 deletions coderd/rbac/policy.rego
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package authz
import future.keywords.in
import future.keywords.every
import future.keywords
# 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


# A great playground: https://play.openpolicyagent.org/
# TODO: Add debug instructions to do in the cli. Running really short on time, the
# playground is sufficient for now imo. In the future we can provide a tidy bash
# script for running this against predefined input.

# bool_flip lets you assign a value to an inverted bool.
# You cannot do 'x := !false', but you can do 'x := bool_flip(false)'
Expand All @@ -19,121 +18,95 @@ bool_flip(b) = flipped {
flipped = true
}

# perms_grant returns a set of boolean values {true, false}.
# True means a positive permission in the set, false is a negative permission.
# It will only return `bool_flip(perm.negate)` for permissions that affect a given
# resource_type, and action.
# The empty set is returned if no relevant permissions are found.
perms_grant(permissions) = grants {
# If there are no permissions, this value is the empty set {}.
grants := { x |
# All permissions ...
perm := permissions[_]
# Such that the permission action, and type matches
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
x := bool_flip(perm.negate)
}
number(set) = c {
count(set) == 0
c := 0
}

# Site & User are both very simple. We default both to the empty set '{}'. If no permissions are present, then the
# result is the default value.
default site = {}
site = grant {
# Boolean set for all site wide permissions.
grant = { v | # Use set comprehension to remove duplicate values
# For each role, grab the site permission.
# Find the grants on this permission list.
v = perms_grant(input.subject.roles[_].site)[_]
}
number(set) = c {
false in set
c := -1
}

default user = {}
user = grant {
# Only apply user permissions if the user owns the resource
input.object.owner != ""
input.object.owner == input.subject.id
grant = { v |
# For each role, grab the user permissions.
# Find the grants on this permission list.
v = perms_grant(input.subject.roles[_].user)[_]
}
number(set) = c {
not false in set
set[_]
c := 1
}

# Organizations are more complex. If the user has no roles that specifically indicate the org_id of the object,
# then we want to block the action. This is because that means the user is not a member of the org.
# A non-member cannot access any org resources.

# org_member returns the set of permissions associated with a user if the user is a member of the
# organization
org_member = grant {
input.object.org_owner != ""
grant = { v |
v = perms_grant(input.subject.roles[_].org[input.object.org_owner])[_]
default site = 0
site := num {
# relevent are all the permissions that affect the given unknown object
allow := { x |
perm := input.subject.roles[_].site[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
x := bool_flip(perm.negate)
}
num := number(allow)
}

# If a user is not part of an organization, 'org_non_member' is set to true
org_non_member {
input.object.org_owner != ""
# Identify if the user is in the org
roles := input.subject.roles
every role in roles {
not role.org[input.object.org_owner]
}
org_members := { orgID |
input.subject.roles[_].org[orgID]
}

# org is two rules that equate to the following
# if org_non_member { return {false} }
# else { org_member }
#
# It is important both rules cannot be true, as the `org` rules cannot produce multiple outputs.
default org = {}
org = set {
# We have to do !org_non_member because rego rules must evaluate to 'true'
# to have a value set.
# So we do "not not-org-member" which means "subject is in org"
not org_non_member
set = org_member
default org = 0
org := num {
orgPerms := { 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)
}

num := orgPerms[input.object.org_owner]
}

org = set {
org_non_member
set = {false}
# 'org_mem' is set to true if the user is an org member
# or if the object has no org.
org_mem := true {
input.object.org_owner != ""
input.object.org_owner in org_members
}

# The allow block is quite simple. Any set with `false` 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.

# site allow
allow {
# No site wide deny
not false in site
# And all permissions are positive
site[_]
org_mem := true {
input.object.org_owner == ""
}

# OR
default user = 0
user := num {
input.subject.id = input.object.owner
# relevent are all the permissions that affect the given unknown object
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)
}

# org allow
default allow = false
# Site
allow {
# No site or org deny
not false in site
not false in org
# And all permissions are positive
org[_]
site = 1
}

# OR
# Org
allow {
not site = -1
org = 1
}

# user allow
# User
allow {
# No site, org, or user deny
not false in site
not false in org
not false in user
# And all permissions are positive
user[_]
not site = -1
not org = -1
org_mem
user = 1
}