Skip to content

chore: document RBAC usage #14065

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 11 commits into from
Sep 10, 2024
Prev Previous commit
Next Next commit
make fmt
Signed-off-by: Danny Kopping <danny@coder.com>
  • Loading branch information
dannykopping committed Sep 10, 2024
commit 23e01c55444b71df799bb293d5e6596374977202
148 changes: 87 additions & 61 deletions coderd/rbac/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,36 @@

# Overview

> _NOTE: you should probably read [`README.md`](README.md) beforehand, but it's not essential._
> _NOTE: you should probably read [`README.md`](README.md) beforehand, but it's
> not essential._

## Basic structure

RBAC is made up of nouns (the objects which are protected by RBAC rules) and verbs (actions which can be performed on
nouns).<br>
For example, a **workspace** (noun) can be **created** (verb), provided the requester has appropriate permissions.
RBAC is made up of nouns (the objects which are protected by RBAC rules) and
verbs (actions which can be performed on nouns).<br> For example, a
**workspace** (noun) can be **created** (verb), provided the requester has
appropriate permissions.

## Roles

We have a number of roles (some of which have legacy connotations back to v1).

These can be found in `coderd/rbac/roles.go`.

| Role | Description | Example resources (non-exhaustive) |
|----------------------|--------------------------------------------------------------------|----------------------------------------------|
| **owner** | Super-user, first user in Coder installation, has all* permissions | all* |
| **member** | A regular user | workspaces, own details, provisioner daemons |
| **auditor** | Viewer of audit log events, read-only access to a few resources | audit logs, templates, users, groups |
| **templateAdmin** | Administrator of templates, read-only access to a few resources | templates, workspaces, users, groups |
| **userAdmin** | Administrator of users | users, groups, role assignments |
| **orgAdmin** | Like **owner**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgMember** | Like **member**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgAuditor** | Like **auditor**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgUserAdmin** | Like **userAdmin**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgTemplateAdmin** | Like **templateAdmin**, but scoped to a single organization | _(org-level equivalent)_ |

_* except some, which are not important to this overview_
| Role | Description | Example resources (non-exhaustive) |
| -------------------- | ------------------------------------------------------------------- | -------------------------------------------- |
| **owner** | Super-user, first user in Coder installation, has all\* permissions | all\* |
| **member** | A regular user | workspaces, own details, provisioner daemons |
| **auditor** | Viewer of audit log events, read-only access to a few resources | audit logs, templates, users, groups |
| **templateAdmin** | Administrator of templates, read-only access to a few resources | templates, workspaces, users, groups |
| **userAdmin** | Administrator of users | users, groups, role assignments |
| **orgAdmin** | Like **owner**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgMember** | Like **member**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgAuditor** | Like **auditor**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgUserAdmin** | Like **userAdmin**, but scoped to a single organization | _(org-level equivalent)_ |
| **orgTemplateAdmin** | Like **templateAdmin**, but scoped to a single organization | _(org-level equivalent)_ |

_\* except some, which are not important to this overview_

## Actions

Expand All @@ -38,7 +40,7 @@ Roles are collections of permissions (we call them _actions_).
These can be found in `coderd/rbac/policy/policy.go`.

| Action | Description |
|-------------------------|-----------------------------------------|
| ----------------------- | --------------------------------------- |
| **create** | Create a resource |
| **read** | Read a resource |
| **update** | Update a resource |
Expand All @@ -55,15 +57,19 @@ These can be found in `coderd/rbac/policy/policy.go`.

# Creating a new noun

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)_.
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)_.

_Refer to https://github.com/coder/coder/pull/14055 to see a full implementation._
_Refer to https://github.com/coder/coder/pull/14055 to see a full
implementation._

## Creating a new entity

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.
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.

Let's say we're adding a new table called `frobulators` (we'll use this table later):
Let's say we're adding a new table called `frobulators` (we'll use this table
later):

```sql
CREATE TABLE frobulators
Expand Down Expand Up @@ -92,15 +98,17 @@ Let's now add our frobulator noun to `coderd/rbac/policy/policy.go`:
...
```

Entries in the `frobulators` table be created/read/updated/deleted, so we define those actions.
Entries in the `frobulators` table be created/read/updated/deleted, so we define
those actions.

`policy.go` is used to generate code in `coderd/rbac/object_gen.go`, and we can execute this by running `make gen`.
`policy.go` is used to generate code in `coderd/rbac/object_gen.go`, and we can
execute this by running `make gen`.

Now we have this change in `coderd/rbac/object_gen.go`:

```go
...
// ResourceFrobulator
// ResourceFrobulator
// Valid Actions
// - "ActionCreate" ::
// - "ActionDelete" ::
Expand All @@ -109,44 +117,48 @@ Now we have this change in `coderd/rbac/object_gen.go`:
ResourceFrobulator = Object{
Type: "frobulator",
}
...
...

func AllResources() []Objecter {
...
ResourceFrobulator,
...
}
func AllResources() []Objecter {
...
ResourceFrobulator,
...
}
```

This creates a resource which represents this noun, and adds it to a list of all available resources.
This creates a resource which represents this noun, and adds it to a list of all
available resources.

## Role Assignment

In our case, we want **members** to be able to CRUD their own frobulators and we want **owners** to CRUD all members' frobulators.
This is how most resources work, and the RBAC system is setup for this by default.
In our case, we want **members** to be able to CRUD their own frobulators and we
want **owners** to CRUD all members' frobulators. This is how most resources
work, and the RBAC system is setup for this by default.

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`:
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`:

```go
func ReloadBuiltinRoles(opts *RoleOptions) {
...
orgAuditor: func(organizationID uuid.UUID) Role {
...
return Role{
...
...
Org: map[string][]Permission{
organizationID.String(): Permissions(map[string][]policy.Action{
...
ResourceFrobulator.Type: {policy.ActionRead},
})
...
...
...
}
```

## Testing

The RBAC system is configured to test all possible actions on all available resources.
The RBAC system is configured to test all possible actions on all available
resources.

Let's run the RBAC test suite:

Expand Down Expand Up @@ -181,10 +193,13 @@ FAIL github.com/coder/coder/v2/coderd/rbac 1.314s
FAIL
```

The message `remaining permissions should be empty for type "frobulator"` indicates that we're missing tests which validate
the desired actions on our new noun.
The message `remaining permissions should be empty for type "frobulator"`
indicates that we're missing tests which validate the desired actions on our new
noun.

> Take a look at `coderd/rbac/roles_test.go` in the [reference PR](https://github.com/coder/coder/pull/14055) for a complete example
> Take a look at `coderd/rbac/roles_test.go` in the
> [reference PR](https://github.com/coder/coder/pull/14055) for a complete
> example

Let's add a test case:

Expand Down Expand Up @@ -218,11 +233,13 @@ func TestRolePermissions(t *testing.T) {
},
```

Note how the `FrobulatorsModify` test case is just validating the `policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete` actions,
and only the **orgMember**, **orgAdmin**, and **owner** can access it.
Note how the `FrobulatorsModify` test case is just validating the
`policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete` actions, and
only the **orgMember**, **orgAdmin**, and **owner** can access it.

Similarly, the `FrobulatorsReadOnly` test case is only validating `policy.ActionRead`, which is allowed on all of the above
plus the **orgAuditor** role.
Similarly, the `FrobulatorsReadOnly` test case is only validating
`policy.ActionRead`, which is allowed on all of the above plus the
**orgAuditor** role.

Now the tests pass, because we have covered all the possible scenarios:

Expand All @@ -231,7 +248,8 @@ $ go test github.com/coder/coder/v2/coderd/rbac -count=1
ok github.com/coder/coder/v2/coderd/rbac 1.313s
```

When a case is not covered, you'll see an error like this (I moved the `orgAuditor` option from `true` to `false):
When a case is not covered, you'll see an error like this (I moved the
`orgAuditor` option from `true` to `false):

```bash
--- FAIL: TestRolePermissions (0.79s)
Expand All @@ -246,10 +264,11 @@ FAIL github.com/coder/coder/v2/coderd/rbac 1.390s
FAIL
```

This shows you that the `org_auditor` role has `read` permissions on the frobulator, but no test case covered it.
This shows you that the `org_auditor` role has `read` permissions on the
frobulator, but no test case covered it.

**NOTE: don't just add cases which make the tests pass; consider all the way in which your resource must be used, and test
all of those scenarios!**
**NOTE: don't just add cases which make the tests pass; consider all the way in
which your resource must be used, and test all of those scenarios!**

# Database authorization

Expand All @@ -264,7 +283,8 @@ FROM frobulators
WHERE user_id = @user_id::uuid;
```

Once we run `make gen`, we'll find some stubbed code in `coderd/database/dbauthz/dbauthz.go`.
Once we run `make gen`, we'll find some stubbed code in
`coderd/database/dbauthz/dbauthz.go`.

```go
...
Expand All @@ -287,17 +307,22 @@ func (q *querier) GetUserFrobulators(ctx context.Context, userID uuid.UUID) ([]d
...
```

This states that the `policy.ActionRead` permission is required in this query on the `ResourceFrobulator` resources,
and `WithOwner(userID.String())` specifies that this user must own the resource.
This states that the `policy.ActionRead` permission is required in this query on
the `ResourceFrobulator` resources, and `WithOwner(userID.String())` specifies
that this user must own the resource.

All queries are executed through `dbauthz`, and now our little frobulators are protected!
All queries are executed through `dbauthz`, and now our little frobulators are
protected!

# API authorization

API authorization is not strictly required because we have database authorization in place, but it's a good practice to
reject requests as soon as possible when the requester is unprivileged.
API authorization is not strictly required because we have database
authorization in place, but it's a good practice to reject requests as soon as
possible when the requester is unprivileged.

> Take a look at `coderd/frobulators.go` in the [reference PR](https://github.com/coder/coder/pull/14055) for a complete example
> Take a look at `coderd/frobulators.go` in the
> [reference PR](https://github.com/coder/coder/pull/14055) for a complete
> example

```go
...
Expand All @@ -312,7 +337,8 @@ func (api *API) listUserFrobulators(rw http.ResponseWriter, r *http.Request) {
}
```

`api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String()))` is specifying that we only
want to permit a user to read their own frobulators. If the requester does not have this permission, we forbid the request.
We're checking the user associated to the API key here because this could also be an **owner** or **orgAdmin**, and we want to
permit those users.
`api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String()))`
is specifying that we only want to permit a user to read their own frobulators.
If the requester does not have this permission, we forbid the request. We're
checking the user associated to the API key here because this could also be an
**owner** or **orgAdmin**, and we want to permit those users.