# Using RBAC ## Overview > _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).
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)_ | **Note an example resource indicates the role has at least 1 permission related to the resource. Not that the role has complete CRUD access to the resource.** _\* except some, which are not important to this overview_ ## Actions 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 | | **delete** | Delete a resource | | **use** | Use a resource | | **read_personal** | Read owned resource | | **update_personal** | Update owned resource | | **ssh** | SSH into a workspace | | **application_connect** | Connect to workspace apps via a browser | | **view_insights** | View deployment insights | | **start** | Start a workspace | | **stop** | Stop a workspace | | **assign** | Assign user to role / org | ## 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)_. _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 acted upon 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): ```sql CREATE TABLE frobulators ( id uuid NOT NULL, user_id uuid NOT NULL, org_id uuid NOT NULL, model_number TEXT NOT NULL, PRIMARY KEY (id), UNIQUE (model_number), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (org_id) REFERENCES organizations (id) ON DELETE CASCADE ); ``` Let's now add our frobulator noun to `coderd/rbac/policy/policy.go`: ```go ... "frobulator": { Actions: map[Action]ActionDefinition{ ActionCreate: {Description: "create a frobulator"}, ActionRead: {Description: "read a frobulator"}, ActionUpdate: {Description: "update a frobulator"}, ActionDelete: {Description: "delete a frobulator"}, }, }, ... ``` We need to create/read/update/delete rows in the `frobulators` table, 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`. Now we have this change in `coderd/rbac/object_gen.go`: ```go ... // ResourceFrobulator // Valid Actions // - "ActionCreate" :: // - "ActionDelete" :: // - "ActionRead" :: // - "ActionUpdate" :: ResourceFrobulator = Object{ Type: "frobulator", } ... func AllResources() []Objecter { ... ResourceFrobulator, ... } ``` 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. However, let's say we want **organization auditors** to have read-only access to all organization's frobulators; we need to add it to `coderd/rbac/roles.go`: ```go func ReloadBuiltinRoles(opts *RoleOptions) { ... auditorRole := Role{ Identifier: RoleAuditor(), DisplayName: "Auditor", Site: Permissions(map[string][]policy.Action{ ... // The site-wide auditor is allowed to read *all* frobulators, regardless of who owns them. ResourceFrobulator.Type: {policy.ActionRead}, ... // orgAuditor: func(organizationID uuid.UUID) Role { ... return Role{ ... Org: map[string][]Permission{ organizationID.String(): Permissions(map[string][]policy.Action{ ... // The org-wide auditor is allowed to read *all* frobulators in their own org, regardless of who owns them. ResourceFrobulator.Type: {policy.ActionRead}, }) ... ... } ``` Note how we added the permission to both the **site-wide** auditor role and the **org-level** auditor role. ## Testing The RBAC system is configured to test all possible actions on all available resources. Let's run the RBAC test suite: `go test github.com/coder/coder/v2/coderd/rbac` We'll see a failure like this: ```bash --- FAIL: TestRolePermissions (0.61s) --- FAIL: TestRolePermissions/frobulator-AllActions (0.00s) roles_test.go:705: Error Trace: /tmp/coder/coderd/rbac/roles_test.go:705 Error: Not equal: expected: map[policy.Action]bool{} actual : map[policy.Action]bool{"create":true, "delete":true, "read":true, "update":true} Diff: --- Expected +++ Actual @@ -1,2 +1,6 @@ -(map[policy.Action]bool) { +(map[policy.Action]bool) (len=4) { + (policy.Action) (len=6) "create": (bool) true, + (policy.Action) (len=6) "delete": (bool) true, + (policy.Action) (len=4) "read": (bool) true, + (policy.Action) (len=6) "update": (bool) true } Test: TestRolePermissions/frobulator-AllActions Messages: remaining permissions should be empty for type "frobulator" FAIL 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. > 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: ```go func TestRolePermissions(t *testing.T) { ... { // Users should be able to modify their own frobulators // Admins from the current organization should be able to modify any other members' frobulators // Owner should be able to modify any other user's frobulators Name: "FrobulatorsModify", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceFrobulator.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {orgMemberMe, orgAdmin, owner}, false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { // Admins from the current organization should be able to read any other members' frobulators // Auditors should be able to read any other members' frobulators // Owner should be able to read any other user's frobulators Name: "FrobulatorsReadAnyUserInOrg", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceFrobulator.WithOwner(uuid.New().String()).InOrg(orgID), // read frobulators of any user AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAuditor}, false: {memberMe, orgMemberMe, setOtherOrg, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin}, }, }, ``` 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. The `FrobulatorsReadAnyUserInOrg` test case is validating that owners, org admins & auditors have the `policy.ActionRead` policy which enables them to read frobulators belonging to any user in a given organization. The above tests are illustrative not exhaustive, see [the reference PR](https://github.com/coder/coder/pull/14055) for the rest. Once we have covered all the possible scenarios, the tests will pass: ```bash $ 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`): ```bash --- FAIL: TestRolePermissions (0.79s) --- FAIL: TestRolePermissions/FrobulatorsReadOnly (0.01s) roles_test.go:737: Error Trace: /tmp/coder/coderd/rbac/roles_test.go:737 Error: An error is expected but got nil. Test: TestRolePermissions/FrobulatorsReadOnly Messages: Should fail: FrobulatorsReadOnly as "org_auditor" doing "read" on "frobulator" FAIL 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. **NOTE: don't just add cases which make the tests pass; consider all the ways in which your resource must be used, and test all of those scenarios!** ## Database authorization Now that we have the RBAC system fully configured, we need to make use of it. Let's add a SQL query to `coderd/database/queries/frobulators.sql`: ```sql -- name: GetFrobulators :many SELECT * FROM frobulators WHERE user_id = $1 AND org_id = $2; ``` Once we run `make gen`, we'll find some stubbed code in `coderd/database/dbauthz/dbauthz.go`. ```go ... func (q *querier) GetFrobulators(ctx context.Context, arg database.GetFrobulatorsParams) ([]database.Frobulator, error) { panic("not implemented") } ... ``` Let's modify this function: ```go ... func (q *querier) GetFrobulators(ctx context.Context, arg database.GetFrobulatorsParams) ([]database.Frobulator, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetFrobulators)(ctx, arg) } ... ``` This states that the `policy.ActionRead` permission is enforced on all entries returned from the database, ensuring that each requested frobulator is readable by the given actor. In order for this to work, we need to implement the `rbac.Objector` interface. `coderd/database/modelmethods.go` is where we implement this interface for all RBAC objects: ```go func (f Frobulator) RBACObject() rbac.Object { return rbac.ResourceFrobulator. WithID(f.ID). // Each frobulator has a unique identity. WithOwner(f.UserID.String()). // It is owned by one and only one user. InOrg(f.OrgID) // It belongs to an organization. } ``` These values obviously have to be set on the `Frobulator` instance before this function can work, hence why we have to fetch the object from the store first before we validate (this explains the `fetchWithPostFilter` naming). 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. > Take a look at `coderd/frobulators.go` in the > [reference PR](https://github.com/coder/coder/pull/14055) for a complete > example ```go ... func (api *API) createFrobulator(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() member := httpmw.OrganizationMemberParam(r) org := httpmw.OrganizationParam(r) var req codersdk.InsertFrobulatorRequest if !httpapi.Read(ctx, rw, r, &req) { return } frob, err := api.Database.InsertFrobulator(ctx, database.InsertFrobulatorParams{ ID: uuid.New(), UserID: member.UserID, OrgID: org.ID, ModelNumber: req.ModelNumber, }) // This will catch forbidden errors as well. if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) return } ... ``` If we look at the implementation of `httpapi.Is404Error`: ```go // Is404Error returns true if the given error should return a 404 status code. // Both actual 404s and unauthorized errors should return 404s to not leak // information about the existence of resources. func Is404Error(err error) bool { if err == nil { return false } // This tests for dbauthz.IsNotAuthorizedError and rbac.IsUnauthorizedError. if IsUnauthorizedError(err) { return true } return xerrors.Is(err, sql.ErrNoRows) } ``` With this, we're able to handle unauthorized access to the resource but return a `404 Not Found` to not leak the fact that the resources exist but are not accessible by the given actor.