Skip to content

Commit 44c02a1

Browse files
committed
chore: add documentation for authz and authztest
1 parent ee8bf04 commit 44c02a1

File tree

2 files changed

+209
-2
lines changed

2 files changed

+209
-2
lines changed

coderd/authz/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Authz
2+
3+
Package `authz` implements AuthoriZation for Coder.
4+
5+
## Overview
6+
7+
Authorization defines what **permission** an **subject** has to perform **actions** to **resources**:
8+
- **Permission** is binary: *yes* (allowed) or *no* (denied).
9+
- **Subject** in this case is anything that implements interface `authz.Subject`.
10+
- **Action** here is an enumerated list of actions, but we stick to `Create`, `Read`, `Update`, and `Delete` here.
11+
- **Resource** here is anything that implements `authz.Resource`.
12+
13+
## Permission Structure
14+
15+
A **permission** is a rule that grants or denies access for a **subject** to perform an **action** on a **resource**.
16+
A **permission** is always applied at a given **level**:
17+
18+
- **site** level applies to all resources in a given Coder deployment.
19+
- **org** level applies to all resources that have an organization owner (`org_owner`)
20+
- **user** level applies to all resources that have an owner with the same ID as the subject.
21+
22+
**Permissions** at a higher **level** always override permissions at a **lower** level.
23+
24+
The effect of a **permission** can be:
25+
- **positive** (allows)
26+
- **negative** (denies)
27+
- **abstain** (neither allows or denies, but interpreted as deny by default)
28+
29+
**Negative** permissions **always** override **positive** permissions at the same level.
30+
Both **negative** and **positive** permissions override **abstain** at the same level.
31+
32+
This can be represented by the following truth table, where Y represents *positive*, N represents *negative*, and _ represents *abstain*:
33+
34+
| Action | Positive | Negative | Result |
35+
|--------|----------|----------|--------|
36+
| read | Y | _ | Y |
37+
| read | Y | N | N |
38+
| read | _ | _ | _ |
39+
| read | _ | N | Y |
40+
41+
42+
## Permission Representation
43+
44+
**Permissions** are represented in string format as `<sign>?<level>.<resource>.<id>.<action>`, where:
45+
46+
- `sign` can be either `+` or `-`. If it is omitted, sign is assumed to be `+`.
47+
- `level` is either `*`, `site`, `org`, or `user`.
48+
- `resource` is any valid resource type.
49+
- `id` is any valid UUID v4.
50+
- `action` is `create`, `read`, `modify`, or `delete`.
51+
52+
## Example Permissions
53+
54+
- `+site.devurl.*.read`: allowed to perform the `read` action against all resources of type `devurl` in a given Coder deployment.
55+
- `-user.workspace.*.create`: user is not allowed to create workspaces.
56+
57+
## Roles
58+
59+
A *role* is a set of permissions. When evaluating a role's permission to form an action, all the relevant permissions for the role are combined at each level. Permissions at a higher level override permissions at a lower level.
60+
61+
The following table shows the per-level role evaluation.
62+
Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _ indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result.
63+
64+
| Role (example) | Site | Org | User | Result |
65+
|-----------------|------|-----|------|--------|
66+
| site-admin | Y | YN_ | YN_ | Y |
67+
| no-permission | N | YN_ | YN_ | N |
68+
| org-admin | _ | Y | YN_ | Y |
69+
| non-org-member | _ | N | YN_ | N |
70+
| user | _ | _ | Y | Y |
71+
| | _ | _ | N | N |
72+
| unauthenticated | _ | _ | _ | N |
73+

coderd/authz/authztest/README.md

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,93 @@
11
# Authztest
22

3-
An authz permission is a combination of `level`, `resource_type`, `resource_id`, and `action`. For testing purposes, we can assume only 1 action and resource exists. This package can generate all possible permissions from this.
3+
Package `authztest` implements _exhaustive_ unit testing for the `authz` package.
44

5-
A `Set` is a slice of permissions. The search space of all possible sets is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance.
5+
## Why this exists
6+
7+
The `authz.Authorize` function has three* inputs:
8+
- Subject (for example, a user or API key)
9+
- Resource (for example, a workspace or a DevURL)
10+
- Action (for example, read or write).
11+
12+
**Not including the ruleset, which we're keeping static for the moment.*
13+
14+
Normally to test a pure function like this, you'd write a table test with all of the permutations by hand, for example:
15+
16+
```go
17+
func Test_Authorize(t *testing.T) {
18+
....
19+
testCases := []struct {
20+
name string
21+
subject authz.Subject
22+
resource authz.Resource
23+
action authz.Action
24+
expectedError error
25+
}{
26+
{
27+
name: "site admin can write config",
28+
subject: &User{ID: "admin"},
29+
object: &authz.ZObject{
30+
OrgOwner: "default",
31+
ResourceType: authz.ResourceSiteConfig,
32+
},
33+
expectedError: nil,
34+
},
35+
...
36+
}
37+
for _, testCase := range testCases {
38+
t.Run(testCase.Name, func(t *testing.T) { ... })
39+
}
40+
}
41+
```
42+
43+
This approach is problematic because of the cardinality of the RBAC model.
44+
45+
Recall that the legacy `pkg/access/authorize`:
46+
- Exposes 8 possible actions, 5 possible site-level roles, 4 possible org-level roles, and 24 possible resource types
47+
- Enforces site-wide versus organization-wide permissions separately
48+
49+
The new authentication model must maintain backward compatibility with this model, whilst allowing additional features such as:
50+
- User-level ownership (which means user-level permission enforcement)
51+
- Resources shared between users (which means permissions granular down to resource IDs)
52+
- Custom roles
53+
54+
The resulting permissions model ([documented in Notion](https://www.notion.so/coderhq/Workspaces-V2-Authz-RBAC-24fd193386eb4cf79a282a2a69e8f917)) results in a large **finite** solution space in the order of **hundreds of millions**.
55+
56+
We want to have a high level of confidence that changes to the implementation **do not have unintended side-effects**.
57+
This means that simply manually writing a set of test cases possibly risks errors slipping through the cracks.
58+
59+
Instead, we generate (almost) all possible sets of inputs to the library, and ensure that `authz.Authorize` performs as expected.
60+
61+
The actual investigation of the solution space is [documented in Notion](https://www.notion.so/coderhq/Authz-Exhaustive-Testing-7683ea694c6e4c12ab0124439916b13a), but the crucial take-away of that document is:
62+
- There is a **large** but **finite** number of possible inputs to `authz.Authorize`,
63+
- The solution space can be broken down into 9 groups, and
64+
- Most importantly, *each group has the same expected result.*
65+
66+
## Testing Methodology
67+
68+
69+
We group the search space into a number of groups. Each group corresponds to a set of test cases with the same expected result. Each group consists of a set of **impactful** permissions and a set of **noise** permissions.
70+
71+
**Impactful** permissions are the top-level permissions that are expected to override anything else, and should be the only inputs that determine the expected result.
72+
**Noise** is simply a set of additional permissions at a lower level that *should not* be impactful.
73+
74+
For each group, we take the **impactful set** of permissions, and add **noise**, and combine this into a role.
75+
76+
We then take the *set cross-product* of the **impactful set** and the **noise**, and assert that the expected access level of that role to perform a given action.
77+
78+
As some of these sets are quite large, we sample some of the noise to reduce the search space.
79+
80+
TODO: example.
81+
82+
## Permission Permutations
83+
84+
Recall that we define a permission as a 4-tuple of `(level, resource_type, resource_id, action)` (for example, `(site, workspace, 123, read)`).
85+
86+
A `Set` is a slice of permissions. The search space of all possible permissions is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance.
687

788
This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `<nil>` permission. The resulting search space for a row is the cross product between all sets in said row.
889

90+
991
| Row | * | Site | Org | Org:mem | User | Access |
1092
|-----|------|------|------|---------|------|--------|
1193
| W+ | Y+_ | YN_ | YN_ | YN_ | YN_ | Y |
@@ -21,3 +103,55 @@ This is the final pruned search space used in authz. Each set is represented by
21103
| A+ | _ | _ | _ | _ | Y+_ | Y |
22104
| A- | _ | _ | _ | _ | _ | N |
23105

106+
Each row in the above table corresponds to a set of test cases. These are described in the next section.
107+
108+
## Test Cases
109+
110+
There are 12 possible permutations.
111+
112+
### Case 1: W+
113+
114+
TODO
115+
116+
### Case 2: W-
117+
118+
TODO
119+
120+
### Case 3: S+
121+
122+
TODO
123+
124+
### Case 4: S-
125+
126+
TODO
127+
128+
### Case 5: O+
129+
130+
TODO
131+
132+
### Case 6: O-
133+
134+
TODO
135+
136+
### Case 7: M+
137+
138+
TODO
139+
140+
### Case 8: M-
141+
142+
TODO
143+
144+
### Case 9: U+
145+
146+
TODO
147+
148+
### Case 10: U-
149+
150+
TODO
151+
152+
### Case 11: A+
153+
154+
TODO
155+
### Case 12: A-
156+
157+
TODO

0 commit comments

Comments
 (0)