Skip to content

Commit cb25bae

Browse files
committed
Document RBAC usage
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent cf1fcab commit cb25bae

File tree

2 files changed

+321
-3
lines changed

2 files changed

+321
-3
lines changed

coderd/rbac/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Authz
22

3-
Package `authz` implements AuthoriZation for Coder.
3+
Package `rbac` implements Role-Based Access Control for Coder.
44

55
## Overview
66

77
Authorization defines what **permission** a **subject** has to perform **actions** to **objects**:
88

99
- **Permission** is binary: _yes_ (allowed) or _no_ (denied).
10-
- **Subject** in this case is anything that implements interface `authz.Subject`.
10+
- **Subject** in this case is anything that implements interface `rbac.Subject`.
1111
- **Action** here is an enumerated list of actions, but we stick to `Create`, `Read`, `Update`, and `Delete` here.
12-
- **Object** here is anything that implements `authz.Object`.
12+
- **Object** here is anything that implements `rbac.Object`.
1313

1414
## Permission Structure
1515

coderd/rbac/USAGE.md

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
# Using RBAC
2+
3+
# Overview
4+
5+
> _NOTE: you should probably read [`README.md`](README.md) beforehand, but it's not essential._
6+
7+
## Basic structure
8+
9+
RBAC is made up of nouns (the objects which are protected by RBAC rules) and verbs (actions which can be performed on
10+
nouns).<br>
11+
For example, a **workspace** (noun) can be **created** (verb), provided the requester has appropriate permissions.
12+
13+
## Roles
14+
15+
We have a number of roles (some of which have legacy connotations back to v1).
16+
17+
These can be found in `coderd/rbac/roles.go`.
18+
19+
| Role | Description | Example resources (non-exhaustive) |
20+
|----------------------|--------------------------------------------------------------------|----------------------------------------------|
21+
| **owner** | Super-user, first user in Coder installation, has all* permissions | all* |
22+
| **member** | A regular user | workspaces, own details, provisioner daemons |
23+
| **auditor** | Viewer of audit log events, read-only access to a few resources | audit logs, templates, users, groups |
24+
| **templateAdmin** | Administrator of templates, read-only access to a few resources | templates, workspaces, users, groups |
25+
| **userAdmin** | Administrator of users | users, groups, role assignments |
26+
| **orgAdmin** | Like **owner**, but scoped to a single organization | _(org-level equivalent)_ |
27+
| **orgMember** | Like **member**, but scoped to a single organization | _(org-level equivalent)_ |
28+
| **orgAuditor** | Like **auditor**, but scoped to a single organization | _(org-level equivalent)_ |
29+
| **orgUserAdmin** | Like **userAdmin**, but scoped to a single organization | _(org-level equivalent)_ |
30+
| **orgTemplateAdmin** | Like **templateAdmin**, but scoped to a single organization | _(org-level equivalent)_ |
31+
32+
_* except some, which are not important to this overview_
33+
34+
## Actions
35+
36+
Roles are collections of permissions (we call them _actions_).
37+
38+
These can be found in `coderd/rbac/policy/policy.go`.
39+
40+
| Action | Description |
41+
|-------------------------|-----------------------------------------|
42+
| **create** | Create a resource |
43+
| **read** | Read a resource |
44+
| **update** | Update a resource |
45+
| **delete** | Delete a resource |
46+
| **use** | Use a resource |
47+
| **read_personal** | Read owned resource |
48+
| **update_personal** | Update owned resource |
49+
| **ssh** | SSH into a workspace |
50+
| **application_connect** | Connect to workspace apps via a browser |
51+
| **view_insights** | View deployment insights |
52+
| **start** | Start a workspace |
53+
| **stop** | Stop a workspace |
54+
| **assign** | Assign user to role / org |
55+
56+
# Creating a new noun
57+
58+
In the following example, we're going to create a new RBAC noun for a new entity called a "frobulator" _(just some nonsense word for demonstration purposes)_.
59+
60+
_Refer to https://github.com/coder/coder/pull/14055 to see a full implementation._
61+
62+
## Creating a new entity
63+
64+
If you're creating a new resource which has to be owned by users of differing roles, you need to create a new RBAC resource.
65+
66+
Let's say we're adding a new table called `frobulators` (we'll use this table later):
67+
68+
```sql
69+
CREATE TABLE frobulators
70+
(
71+
id uuid NOT NULL,
72+
user_id uuid NOT NULL,
73+
model_number TEXT NOT NULL,
74+
PRIMARY KEY (id),
75+
UNIQUE (model_number),
76+
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
77+
);
78+
```
79+
80+
Let's now add our frobulator noun to `coderd/rbac/policy/policy.go`:
81+
82+
```go
83+
...
84+
"frobulator": {
85+
Actions: map[Action]ActionDefinition{
86+
ActionCreate: actDef("create a frobulator"),
87+
ActionRead: actDef("read a frobulator"),
88+
ActionUpdate: actDef("update a frobulator"),
89+
ActionDelete: actDef("delete a frobulator"),
90+
},
91+
},
92+
...
93+
```
94+
95+
Entries in the `frobulators` table be created/read/updated/deleted, so we define those actions.
96+
97+
`policy.go` is used to generate code in `coderd/rbac/object_gen.go`, and we can execute this by running `make gen`.
98+
99+
Now we have this change in `coderd/rbac/object_gen.go`:
100+
101+
```go
102+
...
103+
// ResourceFrobulator
104+
// Valid Actions
105+
// - "ActionCreate" ::
106+
// - "ActionDelete" ::
107+
// - "ActionRead" ::
108+
// - "ActionUpdate" ::
109+
ResourceFrobulator = Object{
110+
Type: "frobulator",
111+
}
112+
...
113+
114+
func AllResources() []Objecter {
115+
...
116+
ResourceFrobulator,
117+
...
118+
}
119+
```
120+
121+
This creates a resource which represents this noun, and adds it to a list of all available resources.
122+
123+
## Role Assignment
124+
125+
In our case, we want **members** to be able to CRUD their own frobulators and we want **owners** to CRUD all members' frobulators.
126+
This is how most resources work, and the RBAC system is setup for this by default.
127+
128+
However, let's say we want **auditors** to have read-only access to all members' frobulators; we need to add it to `coderd/rbac/roles.go`:
129+
130+
```go
131+
func ReloadBuiltinRoles(opts *RoleOptions) {
132+
...
133+
orgAuditor: func(organizationID uuid.UUID) Role {
134+
...
135+
return Role{
136+
...
137+
Org: map[string][]Permission{
138+
organizationID.String(): Permissions(map[string][]policy.Action{
139+
...
140+
ResourceFrobulator.Type: {policy.ActionRead},
141+
})
142+
...
143+
...
144+
}
145+
```
146+
147+
## Testing
148+
149+
The RBAC system is configured to test all possible actions on all available resources.
150+
151+
Let's run the RBAC test suite:
152+
153+
`go test github.com/coder/coder/v2/coderd/rbac`
154+
155+
We'll see a failure like this:
156+
157+
```bash
158+
--- FAIL: TestRolePermissions (0.61s)
159+
--- FAIL: TestRolePermissions/frobulator-AllActions (0.00s)
160+
roles_test.go:705:
161+
Error Trace: /tmp/coder/coderd/rbac/roles_test.go:705
162+
Error: Not equal:
163+
expected: map[policy.Action]bool{}
164+
actual : map[policy.Action]bool{"create":true, "delete":true, "read":true, "update":true}
165+
166+
Diff:
167+
--- Expected
168+
+++ Actual
169+
@@ -1,2 +1,6 @@
170+
-(map[policy.Action]bool) {
171+
+(map[policy.Action]bool) (len=4) {
172+
+ (policy.Action) (len=6) "create": (bool) true,
173+
+ (policy.Action) (len=6) "delete": (bool) true,
174+
+ (policy.Action) (len=4) "read": (bool) true,
175+
+ (policy.Action) (len=6) "update": (bool) true
176+
}
177+
Test: TestRolePermissions/frobulator-AllActions
178+
Messages: remaining permissions should be empty for type "frobulator"
179+
FAIL
180+
FAIL github.com/coder/coder/v2/coderd/rbac 1.314s
181+
FAIL
182+
```
183+
184+
The message `remaining permissions should be empty for type "frobulator"` indicates that we're missing tests which validate
185+
the desired actions on our new noun.
186+
187+
> Take a look at `coderd/rbac/roles_test.go` in the [reference PR](https://github.com/coder/coder/pull/14055) for a complete example
188+
189+
Let's add a test case:
190+
191+
```go
192+
func TestRolePermissions(t *testing.T) {
193+
...
194+
{
195+
// Users should be able to modify their own frobulators
196+
// Admins from the current organization should be able to modify any other user's frobulators
197+
// Owner should be able to modify any other user's frobulators
198+
Name: "FrobulatorsModify",
199+
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
200+
Resource: rbac.ResourceFrobulator.WithOwner(currentUser.String()).InOrg(orgID),
201+
AuthorizeMap: map[bool][]hasAuthSubjects{
202+
true: {orgMemberMe, orgAdmin, owner},
203+
false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
204+
},
205+
},
206+
{
207+
// Users should be able to read their own frobulators
208+
// Admins from the current organization should be able to read any other user's frobulators
209+
// Auditors should be able to read any other user's frobulators
210+
// Owner should be able to read any other user's frobulators
211+
Name: "FrobulatorsReadOnly",
212+
Actions: []policy.Action{policy.ActionRead},
213+
Resource: rbac.ResourceFrobulator.WithOwner(currentUser.String()).InOrg(orgID),
214+
AuthorizeMap: map[bool][]hasAuthSubjects{
215+
true: {orgMemberMe, orgAdmin, owner, orgAuditor},
216+
false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin},
217+
},
218+
},
219+
```
220+
221+
Note how the `FrobulatorsModify` test case is just validating the `policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete` actions,
222+
and only the **orgMember**, **orgAdmin**, and **owner** can access it.
223+
224+
Similarly, the `FrobulatorsReadOnly` test case is only validating `policy.ActionRead`, which is allowed on all of the above
225+
plus the **orgAuditor** role.
226+
227+
Now the tests pass, because we have covered all the possible scenarios:
228+
229+
```bash
230+
$ go test github.com/coder/coder/v2/coderd/rbac -count=1
231+
ok github.com/coder/coder/v2/coderd/rbac 1.313s
232+
```
233+
234+
When a case is not covered, you'll see an error like this (I moved the `orgAuditor` option from `true` to `false):
235+
236+
```bash
237+
--- FAIL: TestRolePermissions (0.79s)
238+
--- FAIL: TestRolePermissions/FrobulatorsReadOnly (0.01s)
239+
roles_test.go:737:
240+
Error Trace: /tmp/coder/coderd/rbac/roles_test.go:737
241+
Error: An error is expected but got nil.
242+
Test: TestRolePermissions/FrobulatorsReadOnly
243+
Messages: Should fail: FrobulatorsReadOnly as "org_auditor" doing "read" on "frobulator"
244+
FAIL
245+
FAIL github.com/coder/coder/v2/coderd/rbac 1.390s
246+
FAIL
247+
```
248+
249+
This shows you that the `org_auditor` role has `read` permissions on the frobulator, but no test case covered it.
250+
251+
**NOTE: don't just add cases which make the tests pass; consider all the way in which your resource must be used, and test
252+
all of those scenarios!**
253+
254+
# Database authorization
255+
256+
Now that we have the RBAC system fully configured, we need to make use of it.
257+
258+
Let's add a SQL query to `coderd/database/queries/frobulators.sql`:
259+
260+
```sql
261+
-- name: GetUserFrobulators :many
262+
SELECT *
263+
FROM frobulators
264+
WHERE user_id = @user_id::uuid;
265+
```
266+
267+
Once we run `make gen`, we'll find some stubbed code in `coderd/database/dbauthz/dbauthz.go`.
268+
269+
```go
270+
...
271+
func (q *querier) GetUserFrobulators(ctx context.Context) ([]database.Frobulator, error) {
272+
panic("not implemented")
273+
}
274+
...
275+
```
276+
277+
Let's modify this function:
278+
279+
```go
280+
...
281+
func (q *querier) GetUserFrobulators(ctx context.Context, userID uuid.UUID) ([]database.Frobulator, error) {
282+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(userID.String())); err != nil {
283+
return nil, err
284+
}
285+
return q.db.GetUserFrobulators(ctx, userID)
286+
}
287+
...
288+
```
289+
290+
This states that the `policy.ActionRead` permission is required in this query on the `ResourceFrobulator` resources,
291+
and `WithOwner(userID.String())` specifies that this user must own the resource.
292+
293+
All queries are executed through `dbauthz`, and now our little frobulators are protected!
294+
295+
# API authorization
296+
297+
API authorization is not strictly required because we have database authorization in place, but it's a good practice to
298+
reject requests as soon as possible when the requester is unprivileged.
299+
300+
> Take a look at `coderd/frobulators.go` in the [reference PR](https://github.com/coder/coder/pull/14055) for a complete example
301+
302+
```go
303+
...
304+
func (api *API) listUserFrobulators(rw http.ResponseWriter, r *http.Request) {
305+
ctx := r.Context()
306+
key := httpmw.APIKey(r)
307+
if !api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String())) {
308+
httpapi.Forbidden(rw)
309+
return
310+
}
311+
...
312+
}
313+
```
314+
315+
`api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String()))` is specifying that we only
316+
want to permit a user to read their own frobulators. If the requester does not have this permission, we forbid the request.
317+
We're checking the user associated to the API key here because this could also be an **owner** or **orgAdmin**, and we want to
318+
permit those users.

0 commit comments

Comments
 (0)