Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
151 changes: 84 additions & 67 deletions coderd/rbac/policy.rego
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package authz
import future.keywords

import rego.v1
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opting into rego.v1


# A great playground: https://play.openpolicyagent.org/
# Helpful cli commands to debug.
# opa eval --format=pretty 'data.authz.allow' -d policy.rego -i input.json
Expand Down Expand Up @@ -29,67 +31,74 @@ import future.keywords

# 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 if {
b
flipped = false
}

bool_flip(b) = flipped {
not b
flipped = true
bool_flip(b) := flipped if {
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 {
number(set) := c if {
count(set) == 0
c := 0
c := 0
}

number(set) = c {
number(set) := c if {
false in set
c := -1
c := -1
}

number(set) = c {
number(set) := c if {
not false in set
set[_]
c := 1
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
default site := 0

site := site_allow(input.subject.roles)

default scope_site := 0

scope_site := site_allow([input.subject.scope])

site_allow(roles) := num {
site_allow(roles) := num if {
# allow is a set of boolean values without duplicates.
allow := { x |
allow := {x |
# Iterate over all site permissions in all roles
perm := roles[_].site[_]
perm.action in [input.action, "*"]
perm := 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)
x := bool_flip(perm.negate)
}
num := number(allow)
}

# org_members is the list of organizations the actor is apart of.
org_members := { orgID |
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
default org := 0

org := org_allow(input.subject.roles)

default scope_org := 0

scope_org := org_allow([input.scope])

# org_allow_set is a helper function that iterates over all orgs that the actor
Expand All @@ -102,10 +111,10 @@ scope_org := org_allow([input.scope])
# The reason we calculate this for all orgs, and not just the input.object.org_owner
# is that sometimes the input.object.org_owner is unknown. In those cases
# we have a list of org_ids that can we use in a SQL 'WHERE' clause.
org_allow_set(roles) := allow_set {
allow_set := { id: num |
org_allow_set(roles) := allow_set if {
allow_set := {id: num |
id := org_members[_]
set := { x |
set := {x |
perm := roles[_].org[id][_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
Expand All @@ -115,7 +124,7 @@ org_allow_set(roles) := allow_set {
}
}

org_allow(roles) := num {
org_allow(roles) := num if {
# If the object has "any_org" set to true, then use the other
# org_allow block.
not input.object.any_org
Expand All @@ -135,78 +144,82 @@ org_allow(roles) := num {
# This is useful for UI elements when we want to conclude, "Can the user create
# a new template in any organization?"
# It is easier than iterating over every organization the user is apart of.
org_allow(roles) := num {
org_allow(roles) := num if {
input.object.any_org # if this is false, this code block is not used
allow := org_allow_set(roles)


# allow is a map of {"<org_id>": <number>}. We only care about values
# that are 1, and ignore the rest.
num := number([
keep |
# for every value in the mapping
value := allow[_]
# only keep values > 0.
# 1 = allow, 0 = abstain, -1 = deny
# We only need 1 explicit allow to allow the action.
# deny's and abstains are intentionally ignored.
value > 0
# result set is a set of [true,false,...]
# which "number()" will convert to a number.
keep := true
keep |
# for every value in the mapping
value := allow[_]

# only keep values > 0.
# 1 = allow, 0 = abstain, -1 = deny
# We only need 1 explicit allow to allow the action.
# deny's and abstains are intentionally ignored.
value > 0

# result set is a set of [true,false,...]
# which "number()" will convert to a number.
keep := true
])
}

# 'org_mem' is set to true if the user is an org member
# If 'any_org' is set to true, use the other block to determine org membership.
org_mem := true {
org_mem if {
not input.object.any_org
input.object.org_owner != ""
input.object.org_owner in org_members
}

org_mem := true {
org_mem if {
input.object.any_org
count(org_members) > 0
}

org_ok {
org_ok if {
org_mem
}

# If the object has no organization, then the user is also considered part of
# the non-existent org.
org_ok {
org_ok if {
input.object.org_owner == ""
not input.object.any_org
}

# 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
default user := 0

user := user_allow(input.subject.roles)

default user_scope := 0

scope_user := user_allow([input.scope])

user_allow(roles) := num {
input.object.owner != ""
input.subject.id = input.object.owner
allow := { x |
perm := roles[_].user[_]
perm.action in [input.action, "*"]
user_allow(roles) := num if {
input.object.owner != ""
input.subject.id = input.object.owner
allow := {x |
perm := roles[_].user[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
x := bool_flip(perm.negate)
}
num := number(allow)
x := bool_flip(perm.negate)
}
num := number(allow)
}

# Scope allow_list is a list of resource IDs explicitly allowed by the scope.
# If the list is '*', then all resources are allowed.
scope_allow_list {
scope_allow_list if {
"*" in input.subject.scope.allow_list
}

scope_allow_list {
scope_allow_list if {
# If the wildcard is listed in the allow_list, we do not care about the
# object.id. This line is included to prevent partial compilations from
# ever needing to include the object.id.
Expand All @@ -226,66 +239,70 @@ scope_allow_list {
# Allow query:
# data.authz.role_allow = true data.authz.scope_allow = true

role_allow {
role_allow if {
site = 1
}

role_allow {
role_allow if {
not site = -1
org = 1
}

role_allow {
role_allow if {
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_ok
user = 1
}

scope_allow {
scope_allow if {
scope_allow_list
scope_site = 1
}

scope_allow {
scope_allow if {
scope_allow_list
not scope_site = -1
scope_org = 1
}

scope_allow {
scope_allow if {
scope_allow_list
not scope_site = -1
not scope_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_ok
scope_user = 1
}

# ACL for users
acl_allow {
acl_allow if {
# Should you have to be a member of the org too?
perms := input.object.acl_user_list[input.subject.id]

# Either the input action or wildcard
[input.action, "*"][_] in perms
}

# ACL for groups
acl_allow {
acl_allow if {
# If there is no organization owner, the object cannot be owned by an
# org_scoped team.
org_mem
group := input.subject.groups[_]
perms := input.object.acl_group_list[group]

# Either the input action or wildcard
[input.action, "*"][_] in perms
}

# ACL for 'all_users' special group
acl_allow {
acl_allow if {
org_mem
perms := input.object.acl_group_list[input.object.org_owner]
[input.action, "*"][_] in perms
Expand All @@ -296,13 +313,13 @@ acl_allow {
# The role or the ACL must allow the action. Scopes can be used to limit,
# so scope_allow must always be true.

allow {
allow if {
role_allow
scope_allow
}

# ACL list must also have the scope_allow to pass
allow {
allow if {
acl_allow
scope_allow
}
24 changes: 13 additions & 11 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ require (
github.com/mocktools/go-smtp-mock/v2 v2.4.0
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
github.com/natefinch/atomic v1.0.1
github.com/open-policy-agent/opa v0.70.0
github.com/open-policy-agent/opa v1.0.0
github.com/ory/dockertest/v3 v3.11.0
github.com/pion/udp v0.1.4
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
Expand All @@ -184,11 +184,11 @@ require (
github.com/wagslane/go-password-validator v0.3.0
go.mozilla.org/pkcs7 v0.9.0
go.nhat.io/otelsql v0.14.0
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0
go.opentelemetry.io/otel/sdk v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
go.opentelemetry.io/otel v1.33.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
go.opentelemetry.io/otel/sdk v1.33.0
go.opentelemetry.io/otel/trace v1.33.0
go.uber.org/atomic v1.11.0
go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29
go.uber.org/mock v0.5.0
Expand Down Expand Up @@ -320,7 +320,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
Expand Down Expand Up @@ -427,21 +427,23 @@ require (
github.com/zclconf/go-cty v1.15.1
github.com/zeebo/errs v1.3.0 // indirect
go.opentelemetry.io/contrib v1.19.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
golang.org/x/time v0.8.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

require go.opentelemetry.io/auto/sdk v1.1.0 // indirect
Loading
Loading