@@ -68,8 +68,8 @@ implementation._
68
68
69
69
## Creating a new entity
70
70
71
- If you're creating a new resource which has to be owned by users of differing
72
- roles, you need to create a new RBAC resource.
71
+ If you're creating a new resource which has to be acted upon by users of
72
+ differing roles, you need to create a new RBAC resource.
73
73
74
74
Let's say we're adding a new table called ` frobulators ` (we'll use this table
75
75
later):
@@ -79,10 +79,12 @@ CREATE TABLE frobulators
79
79
(
80
80
id uuid NOT NULL ,
81
81
user_id uuid NOT NULL ,
82
+ org_id uuid NOT NULL ,
82
83
model_number TEXT NOT NULL ,
83
84
PRIMARY KEY (id),
84
85
UNIQUE (model_number),
85
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
86
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ,
87
+ FOREIGN KEY (org_id) REFERENCES organizations (id) ON DELETE CASCADE
86
88
);
87
89
```
88
90
@@ -92,10 +94,10 @@ Let's now add our frobulator noun to `coderd/rbac/policy/policy.go`:
92
94
...
93
95
" frobulator" : {
94
96
Actions : map [Action]ActionDefinition{
95
- ActionCreate: actDef ( " create a frobulator" ) ,
96
- ActionRead: actDef ( " read a frobulator" ) ,
97
- ActionUpdate: actDef ( " update a frobulator" ) ,
98
- ActionDelete: actDef ( " delete a frobulator" ) ,
97
+ ActionCreate: {Description: " create a frobulator" } ,
98
+ ActionRead: {Description: " read a frobulator" } ,
99
+ ActionUpdate: {Description: " update a frobulator" } ,
100
+ ActionDelete: {Description: " delete a frobulator" } ,
99
101
},
100
102
},
101
103
...
@@ -144,20 +146,34 @@ all organization's frobulators; we need to add it to `coderd/rbac/roles.go`:
144
146
``` go
145
147
func ReloadBuiltinRoles (opts *RoleOptions ) {
146
148
...
149
+ auditorRole := Role{
150
+ Identifier: RoleAuditor (),
151
+ DisplayName: " Auditor" ,
152
+ Site: Permissions (map [string ][]policy.Action {
153
+ ...
154
+ // The site-wide auditor is allowed to read *all* frobulators, regardless of who owns them.
155
+ ResourceFrobulator.Type : {policy.ActionRead },
156
+ ...
157
+
158
+ //
147
159
orgAuditor: func (organizationID uuid.UUID ) Role {
148
160
...
149
161
return Role{
150
162
...
151
163
Org: map [string ][]Permission{
152
164
organizationID.String (): Permissions (map [string ][]policy.Action {
153
165
...
166
+ // The org-wide auditor is allowed to read *all* frobulators in their own org, regardless of who owns them.
154
167
ResourceFrobulator.Type : {policy.ActionRead },
155
168
})
156
169
...
157
170
...
158
171
}
159
172
` ` `
160
173
174
+ Note how we added the permission to both the **site-wide** auditor role and the
175
+ **org-level** auditor role.
176
+
161
177
## Testing
162
178
163
179
The RBAC system is configured to test all possible actions on all available
@@ -222,16 +238,15 @@ func TestRolePermissions(t *testing.T) {
222
238
},
223
239
},
224
240
{
225
- // Users should be able to read their own frobulators
226
241
// Admins from the current organization should be able to read any other members' frobulators
227
242
// Auditors should be able to read any other members' frobulators
228
243
// Owner should be able to read any other user's frobulators
229
- Name: " FrobulatorsReadOnly " ,
244
+ Name: " FrobulatorsReadAnyUserInOrg " ,
230
245
Actions: []policy.Action {policy.ActionRead },
231
- Resource: rbac.ResourceFrobulator .WithOwner (currentUser. String ()).InOrg (orgID),
246
+ Resource: rbac.ResourceFrobulator .WithOwner (uuid. New (). String ()).InOrg (orgID), // read frobulators of any user
232
247
AuthorizeMap: map [bool ][]hasAuthSubjects{
233
- true : {orgMemberMe , orgAdmin, owner , orgAuditor},
234
- false : {setOtherOrg, memberMe , templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin},
248
+ true : {owner , orgAdmin, orgAuditor},
249
+ false : {memberMe, orgMemberMe, setOtherOrg , templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin},
235
250
},
236
251
},
237
252
` ` `
@@ -240,11 +255,14 @@ Note how the `FrobulatorsModify` test case is just validating the
240
255
` policy.ActionCreate , policy.ActionUpdate , policy.ActionDelete ` actions, and
241
256
only the **orgMember**, **orgAdmin**, and **owner** can access it.
242
257
243
- Similarly, the ` FrobulatorsReadOnly` test case is only validating
244
- ` policy.ActionRead ` , which is allowed on all of the above plus the
245
- **orgAuditor** role.
258
+ The ` FrobulatorsReadAnyUserInOrg` test case is validating that owners, org
259
+ admins & auditors have the ` policy.ActionRead ` policy which enables them to read
260
+ frobulators belonging to any user in a given organization.
261
+
262
+ The above tests are illustrative not exhaustive, see
263
+ [the reference PR](https://github.com/coder/coder/pull/14055) for the rest.
246
264
247
- Now the tests pass, because we have covered all the possible scenarios:
265
+ Once we have covered all the possible scenarios, the tests will pass :
248
266
249
267
` ` ` bash
250
268
$ go test github.com /coder/coder/v2/coderd/rbac -count=1
@@ -280,18 +298,18 @@ Now that we have the RBAC system fully configured, we need to make use of it.
280
298
Let' s add a SQL query to ` coderd/database/queries/frobulators.sql` :
281
299
282
300
` ` ` sql
283
- -- name: GetUserFrobulators :many
301
+ -- name: GetFrobulators :many
284
302
SELECT *
285
303
FROM frobulators
286
- WHERE user_id = @user_id::uuid ;
304
+ WHERE user_id = $1 AND org_id = $2 ;
287
305
` ` `
288
306
289
307
Once we run ` make gen` , we' ll find some stubbed code in
290
308
`coderd/database/dbauthz/dbauthz.go`.
291
309
292
310
```go
293
311
...
294
- func (q *querier) GetUserFrobulators (ctx context.Context) ([]database.Frobulator, error) {
312
+ func (q *querier) GetFrobulators (ctx context.Context, arg database.GetFrobulatorsParams ) ([]database.Frobulator, error) {
295
313
panic("not implemented")
296
314
}
297
315
...
@@ -301,15 +319,33 @@ Let's modify this function:
301
319
302
320
` ` ` go
303
321
...
304
- func (q *querier) GetUserFrobulators (ctx context.Context, userID uuid.UUID ) ([]database.Frobulator, error) {
305
- return fetch (q.log, q.auth , q.db.GetUserFrobulators )(ctx, id )
322
+ func (q *querier) GetFrobulators (ctx context.Context, arg database.GetFrobulatorsParams ) ([]database.Frobulator, error) {
323
+ return fetchWithPostFilter (q.auth, policy.ActionRead , q.db.GetFrobulators )(ctx, arg )
306
324
}
307
325
...
308
326
` ` `
309
327
310
- This states that the ` policy.ActionRead` permission is required in this query on
311
- the ` ResourceFrobulator` resources, and ` WithOwner(userID.String())` specifies
312
- that this user must own the resource.
328
+ This states that the ` policy.ActionRead` permission is enforced on all entries
329
+ returned from the database, ensuring that each requested frobulator is readable
330
+ by the given actor.
331
+
332
+ In order for this to work, we need to implement the ` rbac.Objector` interface .
333
+
334
+ ` coderd/database/modelmethods.go` is where we implement this interface for all
335
+ RBAC objects:
336
+
337
+ ` ` ` go
338
+ func (f Frobulator) RBACObject() rbac.Object {
339
+ return rbac.ResourceFrobulator.
340
+ WithID(f.ID). // Each frobulator has a unique identity.
341
+ WithOwner(f.UserID.String()). // It is owned by one and only one user.
342
+ InOrg(f.OrgID) // It belongs to an organization.
343
+ }
344
+ ` ` `
345
+
346
+ These values obviously have to be set on the ` Frobulator` instance before this
347
+ function can work, hence why we have to fetch the object from the store first
348
+ before we validate (this explains the ` fetchWithPostFilter` naming).
313
349
314
350
All queries are executed through ` dbauthz` , and now our little frobulators are
315
351
protected!
@@ -326,19 +362,50 @@ possible when the requester is unprivileged.
326
362
327
363
```go
328
364
...
329
- func (api *API) listUserFrobulators (rw http.ResponseWriter, r *http.Request) {
365
+ func (api *API) createFrobulator (rw http.ResponseWriter, r *http.Request) {
330
366
ctx := r.Context()
331
- key := httpmw.APIKey(r)
332
- if !api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String())) {
333
- httpapi.Forbidden(rw)
367
+ member := httpmw.OrganizationMemberParam(r)
368
+ org := httpmw.OrganizationParam(r)
369
+
370
+ var req codersdk.InsertFrobulatorRequest
371
+ if !httpapi.Read(ctx, rw, r, &req) {
372
+ return
373
+ }
374
+
375
+ frob, err := api.Database.InsertFrobulator(ctx, database.InsertFrobulatorParams{
376
+ ID: uuid.New(),
377
+ UserID: member.UserID,
378
+ OrgID: org.ID,
379
+ ModelNumber: req.ModelNumber,
380
+ })
381
+
382
+ // This will catch forbidden errors as well.
383
+ if httpapi.Is404Error(err) {
384
+ httpapi.ResourceNotFound(rw)
334
385
return
335
386
}
336
387
...
388
+ ```
389
+
390
+ If we look at the implementation of `httpapi.Is404Error`:
391
+
392
+ ```go
393
+ // Is404Error returns true if the given error should return a 404 status code.
394
+ // Both actual 404s and unauthorized errors should return 404s to not leak
395
+ // information about the existence of resources.
396
+ func Is404Error(err error) bool {
397
+ if err == nil {
398
+ return false
399
+ }
400
+
401
+ // This tests for dbauthz.IsNotAuthorizedError and rbac.IsUnauthorizedError.
402
+ if IsUnauthorizedError(err) {
403
+ return true
404
+ }
405
+ return xerrors.Is(err, sql.ErrNoRows)
337
406
}
338
407
```
339
408
340
- `api.Authorize(r, policy.ActionRead, rbac.ResourceFrobulator.WithOwner(key.UserID.String()))`
341
- is specifying that we only want to permit a user to read their own frobulators.
342
- If the requester does not have this permission, we forbid the request. We' re
343
- checking the user associated to the API key here because this could also be an
344
- **owner** or **orgAdmin**, and we want to permit those users.
409
+ With this, we' re able to handle unauthorized access to the resource but return a
410
+ ` 404 Not Found` to not leak the fact that the resources exist but are not
411
+ accessible by the given actor.
0 commit comments