Skip to content

Commit 1d1070d

Browse files
authored
chore: ensure proper rbac permissions on 'Acquire' file in the cache (#18348)
The file cache was caching the `Unauthorized` errors if a user without the right perms opened the file first. So all future opens would fail. Now the cache always opens with a subject that can read files. And authz is checked on the Acquire per user.
1 parent d83706b commit 1d1070d

File tree

16 files changed

+218
-51
lines changed

16 files changed

+218
-51
lines changed

coderd/authorize.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
// objects that the user is authorized to perform the given action on.
2020
// This is faster than calling Authorize() on each object.
2121
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action policy.Action, objects []O) ([]O, error) {
22-
roles := httpmw.UserAuthorization(r)
22+
roles := httpmw.UserAuthorization(r.Context())
2323
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects)
2424
if err != nil {
2525
// Log the error as Filter should not be erroring.
@@ -65,7 +65,7 @@ func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Obj
6565
// return
6666
// }
6767
func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
68-
roles := httpmw.UserAuthorization(r)
68+
roles := httpmw.UserAuthorization(r.Context())
6969
err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject())
7070
if err != nil {
7171
// Log the errors for debugging
@@ -97,7 +97,7 @@ func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object
9797
// call 'Authorize()' on the returned objects.
9898
// Note the authorization is only for the given action and object type.
9999
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
100-
roles := httpmw.UserAuthorization(r)
100+
roles := httpmw.UserAuthorization(r.Context())
101101
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
102102
if err != nil {
103103
return nil, xerrors.Errorf("prepare filter: %w", err)
@@ -120,7 +120,7 @@ func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Actio
120120
// @Router /authcheck [post]
121121
func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
122122
ctx := r.Context()
123-
auth := httpmw.UserAuthorization(r)
123+
auth := httpmw.UserAuthorization(r.Context())
124124

125125
var params codersdk.AuthorizationRequest
126126
if !httpapi.Read(ctx, rw, r, &params) {

coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ func New(options *Options) *API {
572572
TemplateScheduleStore: options.TemplateScheduleStore,
573573
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
574574
AccessControlStore: options.AccessControlStore,
575-
FileCache: files.NewFromStore(options.Database, options.PrometheusRegistry),
575+
FileCache: files.NewFromStore(options.Database, options.PrometheusRegistry, options.Authorizer),
576576
Experiments: experiments,
577577
WebpushDispatcher: options.WebPushDispatcher,
578578
healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{},

coderd/coderdtest/authorize.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ func (r *RecordingAuthorizer) AssertOutOfOrder(t *testing.T, actor rbac.Subject,
234234
// AssertActor asserts in order. If the order of authz calls does not match,
235235
// this will fail.
236236
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
237+
r.AssertActorID(t, actor.ID, did...)
238+
}
239+
240+
func (r *RecordingAuthorizer) AssertActorID(t *testing.T, id string, did ...ActionObjectPair) {
237241
r.Lock()
238242
defer r.Unlock()
239243
ptr := 0
@@ -242,7 +246,7 @@ func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did
242246
// Finished all assertions
243247
return
244248
}
245-
if call.Actor.ID == actor.ID {
249+
if call.Actor.ID == id {
246250
action, object := did[ptr].Action, did[ptr].Object
247251
assert.Equalf(t, action, call.Action, "assert action %d", ptr)
248252
assert.Equalf(t, object, call.Object, "assert object %d", ptr)

coderd/database/dbauthz/dbauthz.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,25 @@ var (
432432
}),
433433
Scope: rbac.ScopeAll,
434434
}.WithCachedASTValue()
435+
436+
subjectFileReader = rbac.Subject{
437+
Type: rbac.SubjectTypeFileReader,
438+
FriendlyName: "Can Read All Files",
439+
// Arbitrary uuid to have a unique ID for this subject.
440+
ID: rbac.SubjectTypeFileReaderID,
441+
Roles: rbac.Roles([]rbac.Role{
442+
{
443+
Identifier: rbac.RoleIdentifier{Name: "file-reader"},
444+
DisplayName: "FileReader",
445+
Site: rbac.Permissions(map[string][]policy.Action{
446+
rbac.ResourceFile.Type: {policy.ActionRead},
447+
}),
448+
Org: map[string][]rbac.Permission{},
449+
User: []rbac.Permission{},
450+
},
451+
}),
452+
Scope: rbac.ScopeAll,
453+
}.WithCachedASTValue()
435454
)
436455

437456
// AsProvisionerd returns a context with an actor that has permissions required
@@ -498,6 +517,10 @@ func AsPrebuildsOrchestrator(ctx context.Context) context.Context {
498517
return As(ctx, subjectPrebuildsOrchestrator)
499518
}
500519

520+
func AsFileReader(ctx context.Context) context.Context {
521+
return As(ctx, subjectFileReader)
522+
}
523+
501524
var AsRemoveActor = rbac.Subject{
502525
ID: "remove-actor",
503526
}

coderd/files/cache.go

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,41 @@ import (
1313

1414
archivefs "github.com/coder/coder/v2/archive/fs"
1515
"github.com/coder/coder/v2/coderd/database"
16+
"github.com/coder/coder/v2/coderd/database/dbauthz"
17+
"github.com/coder/coder/v2/coderd/rbac"
18+
"github.com/coder/coder/v2/coderd/rbac/policy"
1619
"github.com/coder/coder/v2/coderd/util/lazy"
1720
)
1821

1922
// NewFromStore returns a file cache that will fetch files from the provided
2023
// database.
21-
func NewFromStore(store database.Store, registerer prometheus.Registerer) *Cache {
22-
fetch := func(ctx context.Context, fileID uuid.UUID) (cacheEntryValue, error) {
23-
file, err := store.GetFileByID(ctx, fileID)
24+
func NewFromStore(store database.Store, registerer prometheus.Registerer, authz rbac.Authorizer) *Cache {
25+
fetch := func(ctx context.Context, fileID uuid.UUID) (CacheEntryValue, error) {
26+
// Make sure the read does not fail due to authorization issues.
27+
// Authz is checked on the Acquire call, so this is safe.
28+
//nolint:gocritic
29+
file, err := store.GetFileByID(dbauthz.AsFileReader(ctx), fileID)
2430
if err != nil {
25-
return cacheEntryValue{}, xerrors.Errorf("failed to read file from database: %w", err)
31+
return CacheEntryValue{}, xerrors.Errorf("failed to read file from database: %w", err)
2632
}
2733

2834
content := bytes.NewBuffer(file.Data)
29-
return cacheEntryValue{
30-
FS: archivefs.FromTarReader(content),
31-
size: int64(content.Len()),
35+
return CacheEntryValue{
36+
Object: file.RBACObject(),
37+
FS: archivefs.FromTarReader(content),
38+
Size: int64(content.Len()),
3239
}, nil
3340
}
3441

35-
return New(fetch, registerer)
42+
return New(fetch, registerer, authz)
3643
}
3744

38-
func New(fetch fetcher, registerer prometheus.Registerer) *Cache {
45+
func New(fetch fetcher, registerer prometheus.Registerer, authz rbac.Authorizer) *Cache {
3946
return (&Cache{
4047
lock: sync.Mutex{},
4148
data: make(map[uuid.UUID]*cacheEntry),
4249
fetcher: fetch,
50+
authz: authz,
4351
}).registerMetrics(registerer)
4452
}
4553

@@ -101,6 +109,7 @@ type Cache struct {
101109
lock sync.Mutex
102110
data map[uuid.UUID]*cacheEntry
103111
fetcher
112+
authz rbac.Authorizer
104113

105114
// metrics
106115
cacheMetrics
@@ -117,18 +126,19 @@ type cacheMetrics struct {
117126
totalCacheSize prometheus.Counter
118127
}
119128

120-
type cacheEntryValue struct {
129+
type CacheEntryValue struct {
121130
fs.FS
122-
size int64
131+
Object rbac.Object
132+
Size int64
123133
}
124134

125135
type cacheEntry struct {
126136
// refCount must only be accessed while the Cache lock is held.
127137
refCount int
128-
value *lazy.ValueWithError[cacheEntryValue]
138+
value *lazy.ValueWithError[CacheEntryValue]
129139
}
130140

131-
type fetcher func(context.Context, uuid.UUID) (cacheEntryValue, error)
141+
type fetcher func(context.Context, uuid.UUID) (CacheEntryValue, error)
132142

133143
// Acquire will load the fs.FS for the given file. It guarantees that parallel
134144
// calls for the same fileID will only result in one fetch, and that parallel
@@ -146,22 +156,33 @@ func (c *Cache) Acquire(ctx context.Context, fileID uuid.UUID) (fs.FS, error) {
146156
c.Release(fileID)
147157
return nil, err
148158
}
159+
160+
subject, ok := dbauthz.ActorFromContext(ctx)
161+
if !ok {
162+
return nil, dbauthz.ErrNoActor
163+
}
164+
// Always check the caller can actually read the file.
165+
if err := c.authz.Authorize(ctx, subject, policy.ActionRead, it.Object); err != nil {
166+
c.Release(fileID)
167+
return nil, err
168+
}
169+
149170
return it.FS, err
150171
}
151172

152-
func (c *Cache) prepare(ctx context.Context, fileID uuid.UUID) *lazy.ValueWithError[cacheEntryValue] {
173+
func (c *Cache) prepare(ctx context.Context, fileID uuid.UUID) *lazy.ValueWithError[CacheEntryValue] {
153174
c.lock.Lock()
154175
defer c.lock.Unlock()
155176

156177
entry, ok := c.data[fileID]
157178
if !ok {
158-
value := lazy.NewWithError(func() (cacheEntryValue, error) {
179+
value := lazy.NewWithError(func() (CacheEntryValue, error) {
159180
val, err := c.fetcher(ctx, fileID)
160181

161182
// Always add to the cache size the bytes of the file loaded.
162183
if err == nil {
163-
c.currentCacheSize.Add(float64(val.size))
164-
c.totalCacheSize.Add(float64(val.size))
184+
c.currentCacheSize.Add(float64(val.Size))
185+
c.totalCacheSize.Add(float64(val.Size))
165186
}
166187

167188
return val, err
@@ -206,7 +227,7 @@ func (c *Cache) Release(fileID uuid.UUID) {
206227

207228
ev, err := entry.value.Load()
208229
if err == nil {
209-
c.currentCacheSize.Add(-1 * float64(ev.size))
230+
c.currentCacheSize.Add(-1 * float64(ev.Size))
210231
}
211232

212233
delete(c.data, fileID)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy