Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd56df14

Browse files
committed
refactor: update policy.rego and expand RBAC readme
1 parent4954edb commitd56df14

File tree

3 files changed

+170
-55
lines changed

3 files changed

+170
-55
lines changed

‎coderd/rbac/README.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,92 @@ Example of a scope for a workspace agent token, using an `allow_list` containing
102102
}
103103
```
104104

105+
##OPA (Open Policy Agent)
106+
107+
Open Policy Agent (OPA) is an open source tool used to define and enforce policies.
108+
Policies are written in a high-level, declarative language called Rego. Coder’s RBAC rules are defined in the[`policy.rego`](policy.rego) file.
109+
110+
When OPA evaluates policies, it binds input data to a global variable called`input`.
111+
In the`rbac` package, this structured data is defined as JSON and contains the subject, action, and object (see`regoInputValue` in[astvalue.go](astvalue.go)).
112+
OPA evaluates whether the subject is allowed to perform the action on the object across three levels: site, org, and user.
113+
This is determined by the final rule`allow`, defined in[`policy.rego`](policy.rego), which aggregates the results of multiple rules to decide if the user has the necessary permissions.
114+
Similarly to the input, OPA produces structured output data, which includes the`allow` variable as part of the evaluation result.
115+
Authorization succeeds only if`allow` explicitly evaluates to`true`. If no`allow` is returned, it is considered unauthorized.
116+
To learn more about OPA and Rego, seehttps://www.openpolicyagent.org/docs.
117+
118+
###Application and Database Integration
119+
120+
*[`rbac/authz.go`](authz.go) – Application layer integration: provides the core authorization logic that integrates with Rego for policy evaluation.
121+
*[`database/dbauthz/dbauthz.go`](../database/dbauthz/dbauthz.go) – Database layer integration: wraps the database layer with authorization checks to enforce access control.
122+
123+
There are two types of evaluation in OPA:
124+
***Full evaluation**: Produces a decision that can be enforced.
125+
This is the default evaluation mode, where OPA evaluates the policy using`input` data that contains all known values and returns output data with the`allow` variable.
126+
***Partial evaluation**: Produces a new policy that can be evaluated later when the_unknowns_ become_known_.
127+
This is an optimization in OPA where it evaluates as much of the policy as possible without resolving expressions that depend on_unknown_ values from the`input`.
128+
To learn more about partial evaluation, see this[OPA blog post](https://blog.openpolicyagent.org/partial-evaluation-162750eaf422).
129+
130+
Application of Full and Partial evaluation in`rbac` package:
131+
***Full Evaluation** is handled by the`RegoAuthorizer.Authorize()` method in`authz.go`.
132+
This method determines whether a subject (user) can perform a specific action on an object.
133+
It performs a full evaluation of the Rego policy, which returns the`allow` variable to decide whether access is granted or denied (`true` or`false`, respectively).
134+
***Partial Evaluation** is handled by the`RegoAuthorizer.Prepare()` method in`authz.go`.
135+
This method compiles Rego’s partial evaluation queries into`SQL WHERE` clauses.
136+
These clauses are then used to enforce authorization directly in database queries, rather than in application code.
137+
138+
Authorization Patterns:
139+
* Fetch-then-authorize: an object is first retrieved from the database, and a single authorization check is performed using full evaluation via`Authorize()`.
140+
* Authorize-while-fetching: Partial evaluation via`Prepare()` is used to inject SQL filters directly into queries, allowing efficient authorization of many objects of the same type.
141+
`dbauthz` methods that enforce authorization directly in the SQL query are prefixed with`Authorized`, for example,`GetAuthorizedWorkspaces`.
142+
105143
##Testing
106144

107-
You can test outside of golang by using the`opa` cli.
145+
* OPA Playground:https://play.openpolicyagent.org/
146+
* OPA CLI (`opa eval`): useful for experimenting with different inputs and understanding how the policy behaves under various conditions.
147+
`opa eval` returns the constraints that must be satisfied for a rule to evaluate to true.
108148

109-
**Evaluation**
149+
**FullEvaluation**
110150

111151
```bash
112152
opaeval --format=pretty"data.authz.allow" -d policy.rego -i input.json
113153
```
114154

155+
This command fully evaluates the policy in the`policy.rego` file using the input data from`input.json`, and returns the result of the`allow` variable:
156+
*`data.authz.allow` accesses the`allow` rule within the`authz` package.
157+
*`data.authz` on its own would return the entire output object of the package.
158+
159+
This command answers the question: “Is the user allowed?”
160+
115161
**Partial Evaluation**
116162

117163
```bash
118164
opaeval --partial --format=pretty'data.authz.allow' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
119165
```
166+
167+
This command performs a partial evaluation of the policy, specifying a set of unknown input parameters.
168+
The result is a set of partial queries that can be converted into`SQL WHERE` clauses and injected into SQL queries.
169+
170+
This command answers the question: “What conditions must be met for the user to be allowed?”
171+
172+
###Benchmarking
173+
174+
Benchmark tests to evaluate the performance of full and partial evaluation can be found in`authz_test.go`.
175+
You can run these tests with the`-bench` flag, for example:
176+
```bash
177+
gotest -bench=BenchmarkRBACFilter -run=^$
178+
```
179+
180+
To capture memory and CPU profiles, use the following flags:
181+
*`-memprofile memprofile.out`
182+
*`-cpuprofile cpuprofile.out`
183+
184+
The script[`benchmark_authz.sh`](./scripts/benchmark_authz.sh) runs the authz benchmark tests on the current Git branch or compares benchmark results between two branches using[`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat).
185+
`benchstat` compares the performance of a baseline benchmark against a new benchmark result and highlights any statistically significant differences.
186+
* To run benchmark on the current branch:
187+
```bash
188+
benchmark_authz.sh --single
189+
```
190+
* To compare benchmarks between 2 branches:
191+
```bash
192+
benchmark_authz.sh --compare main prebuild_policy
193+
```

‎coderd/rbac/authz_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
148148

149149
// BenchmarkRBACAuthorize benchmarks the rbac.Authorize method.
150150
//
151-
//go test -run=^$ -bench BenchmarkRBACAuthorize -benchmem -memprofile memprofile.out -cpuprofile profile.out
151+
//go test -run=^$ -bench'^BenchmarkRBACAuthorize$' -benchmem -memprofile memprofile.out -cpuprofile profile.out
152152
funcBenchmarkRBACAuthorize(b*testing.B) {
153153
benchCases,user,orgs:=benchmarkUserCases()
154154
users:=append([]uuid.UUID{},
@@ -178,7 +178,7 @@ func BenchmarkRBACAuthorize(b *testing.B) {
178178
// BenchmarkRBACAuthorizeGroups benchmarks the rbac.Authorize method and leverages
179179
// groups for authorizing rather than the permissions/roles.
180180
//
181-
//go test -bench BenchmarkRBACAuthorizeGroups -benchmem -memprofile memprofile.out -cpuprofile profile.out
181+
//go test -bench'^BenchmarkRBACAuthorizeGroups$' -benchmem -memprofile memprofile.out -cpuprofile profile.out
182182
funcBenchmarkRBACAuthorizeGroups(b*testing.B) {
183183
benchCases,user,orgs:=benchmarkUserCases()
184184
users:=append([]uuid.UUID{},
@@ -229,7 +229,7 @@ func BenchmarkRBACAuthorizeGroups(b *testing.B) {
229229

230230
// BenchmarkRBACFilter benchmarks the rbac.Filter method.
231231
//
232-
//go test -bench BenchmarkRBACFilter -benchmem -memprofile memprofile.out -cpuprofile profile.out
232+
//go test -bench'^BenchmarkRBACFilter$' -benchmem -memprofile memprofile.out -cpuprofile profile.out
233233
funcBenchmarkRBACFilter(b*testing.B) {
234234
benchCases,user,orgs:=benchmarkUserCases()
235235
users:=append([]uuid.UUID{},

‎coderd/rbac/policy.rego

Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,76 +29,93 @@ import rego.v1
2929
# different code branches based on the org_owner. 'num's value does, but
3030
# that is the whole point of partial evaluation.
3131

32-
# bool_flip lets you assign a value to an inverted bool.
32+
# bool_flip(b) returns the logical negation of a boolean value 'b'.
3333
# You cannot do 'x := !false', but you can do 'x := bool_flip(false)'
34-
bool_flip(b):=flipped if{
34+
bool_flip(b):=false if{
3535
b
36-
flipped=false
3736
}
3837

39-
bool_flip(b):=flipped if{
38+
bool_flip(b):=true if{
4039
notb
41-
flipped=true
4240
}
4341

44-
# number is a quick way to get a set of {true, false} and convert it to
45-
# -1: {false, true} or {false}
46-
# 0: {}
47-
# 1: {true}
48-
number(set):= c if{
49-
count(set)==0
50-
c:=0
51-
}
42+
# number(set) maps a set of boolean values to one of the following numbers:
43+
# -1: deny (if 'false' value is in the set) => set is {true, false} or {false}
44+
# 0: no decision (if the set is empty) => set is {}
45+
# 1: allow (if only 'true' values are in the set)=> set is {true}
5246

53-
number(set):= c if{
47+
# Return -1 if the set contains any 'false' value (i.e., an explicit deny)
48+
number(set):=-1 if{
5449
false inset
55-
c:=-1
5650
}
5751

58-
number(set):= c if{
52+
# Return 0 if the set is empty (no matching permissions)
53+
number(set):=0 if{
54+
count(set)==0
55+
}
56+
57+
# Return 1 if the set is non-empty and contains no 'false' values (i.e., only allows)
58+
number(set):=1 if{
5959
notfalse inset
6060
set[_]
61-
c:=1
6261
}
6362

64-
# site, org, and user rules are all similar. Each rule should return a number
65-
# from [-1, 1]. The number corresponds to "negative", "abstain", and "positive"
66-
# for the given level. See the 'allow' rules for how these numbers are used.
67-
defaultsite:=0
63+
# Permission evaluation is structured into three levels: site, org, and user.
64+
# For each level, two variables are computed:
65+
# - <level>: the decision based on the subject's full set of roles for that level
66+
# - scope_<level>: the decision based on the subject's scoped roles for that level
67+
#
68+
# Each of these variables is assigned one of three values:
69+
# -1 => negative (deny)
70+
# 0 => abstain (no matching permission)
71+
# 1 => positive (allow)
72+
#
73+
# These values are computed by calling the corresponding <level>_allow functions.
74+
# The final decision is derived from combining these values (see 'allow' rule).
75+
76+
# -------------------
77+
# Site Level Rules
78+
# -------------------
6879

80+
defaultsite:=0
6981
site:=site_allow(input.subject.roles)
7082

7183
defaultscope_site:=0
72-
7384
scope_site:=site_allow([input.subject.scope])
7485

86+
# site_allow receives a list of roles and returns a single number:
87+
# -1 if any matching permission denies access
88+
# 1 if there's at least one allow and no denies
89+
# 0 if there are no matching permissions
7590
site_allow(roles):= num if{
76-
# allow is a set of boolean valueswithoutduplicates.
77-
allow:= {x|
91+
# allow is a set of boolean values(sets don't containduplicates)
92+
allow:= {is_allowed|
7893
# Iterate over all site permissions in all roles
7994
perm:= roles[_].site[_]
8095
perm.action in[input.action,"*"]
8196
perm.resource_type in[input.object.type,"*"]
8297

83-
#x is either 'true' or 'false' if a matching permission exists.
84-
x:=bool_flip(perm.negate)
98+
#is_allowed is either 'true' or 'false' if a matching permission exists.
99+
is_allowed:=bool_flip(perm.negate)
85100
}
86101
num:=number(allow)
87102
}
88103

104+
# -------------------
105+
# Org Level Rules
106+
# -------------------
107+
89108
# org_members is the list of organizations the actor is apart of.
90109
org_members:= {orgID|
91110
input.subject.roles[_].org[orgID]
92111
}
93112

94-
# org is the same as 'site' except we need to iterate over each organization
113+
#'org' is the same as 'site' except we need to iterate over each organization
95114
# that the actor is a member of.
96115
defaultorg:=0
97-
98116
org:=org_allow(input.subject.roles)
99117

100118
defaultscope_org:=0
101-
102119
scope_org:=org_allow([input.scope])
103120

104121
# org_allow_set is a helper function that iterates over all orgs that the actor
@@ -114,11 +131,14 @@ scope_org := org_allow([input.scope])
114131
org_allow_set(roles):= allow_set if{
115132
allow_set:= {id: num|
116133
id:= org_members[_]
117-
set:= {x|
134+
set:= {is_allowed|
135+
# Iterate over all org permissions in all roles
118136
perm:= roles[_].org[id][_]
119137
perm.action in[input.action,"*"]
120138
perm.resource_type in[input.object.type,"*"]
121-
x:=bool_flip(perm.negate)
139+
140+
# is_allowed is either 'true' or 'false' if a matching permission exists.
141+
is_allowed:=bool_flip(perm.negate)
122142
}
123143
num:=number(set)
124144
}
@@ -191,24 +211,30 @@ org_ok if {
191211
notinput.object.any_org
192212
}
193213

194-
# User is the same as the site, except it only applies if the user owns the object and
214+
# -------------------
215+
# User Level Rules
216+
# -------------------
217+
218+
# 'user' is the same as 'site', except it only applies if the user owns the object and
195219
# the user is apart of the org (if the object has an org).
196220
defaultuser:=0
197-
198221
user:=user_allow(input.subject.roles)
199222

200-
defaultuser_scope:=0
201-
223+
defaultscope_user:=0
202224
scope_user:=user_allow([input.scope])
203225

204226
user_allow(roles):= num if{
205227
input.object.owner!=""
206228
input.subject.id= input.object.owner
207-
allow:= {x|
229+
230+
allow:= {is_allowed|
231+
# Iterate over all user permissions in all roles
208232
perm:= roles[_].user[_]
209233
perm.action in[input.action,"*"]
210234
perm.resource_type in[input.object.type,"*"]
211-
x:=bool_flip(perm.negate)
235+
236+
# is_allowed is either 'true' or 'false' if a matching permission exists.
237+
is_allowed:=bool_flip(perm.negate)
212238
}
213239
num:=number(allow)
214240
}
@@ -227,17 +253,9 @@ scope_allow_list if {
227253
input.object.id ininput.subject.scope.allow_list
228254
}
229255

230-
# The allow block is quite simple. Any set with `-1` cascades down in levels.
231-
# Authorization looks for any `allow` statement that is true. Multiple can be true!
232-
# Note that the absence of `allow` means "unauthorized".
233-
# An explicit `"allow": true` is required.
234-
#
235-
# Scope is also applied. The default scope is "wildcard:wildcard" allowing
236-
# all actions. If the scope is not "1", then the action is not authorized.
237-
#
238-
#
239-
# Allow query:
240-
# data.authz.role_allow = true data.authz.scope_allow = true
256+
# -------------------
257+
# Role-Specific Rules
258+
# -------------------
241259

242260
role_allow if{
243261
site=1
@@ -258,6 +276,10 @@ role_allow if {
258276
user=1
259277
}
260278

279+
# -------------------
280+
# Scope-Specific Rules
281+
# -------------------
282+
261283
scope_allow if{
262284
scope_allow_list
263285
scope_site=1
@@ -280,6 +302,11 @@ scope_allow if {
280302
scope_user=1
281303
}
282304

305+
# -------------------
306+
# ACL-Specific Rules
307+
# Access Control List
308+
# -------------------
309+
283310
# ACL for users
284311
acl_allow if{
285312
# Should you have to be a member of the org too?
@@ -308,11 +335,25 @@ acl_allow if {
308335
[input.action,"*"][_] inperms
309336
}
310337

311-
###############
338+
# -------------------
312339
# Final Allow
340+
#
341+
# The 'allow' block is quite simple. Any set with `-1` cascades down in levels.
342+
# Authorization looks for any `allow` statement that is true. Multiple can be true!
343+
# Note that the absence of `allow` means "unauthorized".
344+
# An explicit `"allow": true` is required.
345+
#
346+
# Scope is also applied. The default scope is "wildcard:wildcard" allowing
347+
# all actions. If the scope is not "1", then the action is not authorized.
348+
#
349+
#
350+
# Allow query:
351+
# data.authz.role_allow = true
352+
# data.authz.scope_allow = true
353+
# -------------------
354+
313355
# The role or the ACL must allow the action. Scopes can be used to limit,
314356
# so scope_allow must always be true.
315-
316357
allow if{
317358
role_allow
318359
scope_allow

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp