Skip to content

Commit 27c3ec0

Browse files
authored
perf: support fastpath in dbauthz GetLatestWorkspaceBuildByWorkspaceID (#21047)
This PR piggy backs on the agent API cached workspace added in earlier PRs to provide a fast path for avoiding `GetWorkspaceByID` calls in `GetLatestWorkspaceBuildByWorkspaceID` via injection of the workspaces RBAC object into the context. We can do this from the `agentConnectionMonitor` easily since we already cache the workspace. --------- Signed-off-by: Callum Styan <callumstyan@gmail.com>
1 parent a59a84b commit 27c3ec0

File tree

4 files changed

+118
-4
lines changed

4 files changed

+118
-4
lines changed

coderd/agentapi/cached_workspace.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package agentapi
22

33
import (
4+
"context"
45
"sync"
56

7+
"golang.org/x/xerrors"
8+
69
"github.com/coder/coder/v2/coderd/database"
10+
"github.com/coder/coder/v2/coderd/database/dbauthz"
711
)
812

913
// CachedWorkspaceFields contains workspace data that is safe to cache for the
@@ -50,3 +54,19 @@ func (cws *CachedWorkspaceFields) AsWorkspaceIdentity() (database.WorkspaceIdent
5054
}
5155
return cws.identity, true
5256
}
57+
58+
// ContextInject attempts to inject the rbac object for the cached workspace fields
59+
// into the given context, either returning the wrapped context or the original.
60+
func (cws *CachedWorkspaceFields) ContextInject(ctx context.Context) (context.Context, error) {
61+
var err error
62+
rbacCtx := ctx
63+
if dbws, ok := cws.AsWorkspaceIdentity(); ok {
64+
rbacCtx, err = dbauthz.WithWorkspaceRBAC(ctx, dbws.RBACObject())
65+
if err != nil {
66+
// Don't error level log here, will exit the function. We want to fall back to GetWorkspaceByAgentID.
67+
//nolint:gocritic
68+
return ctx, xerrors.Errorf("Cached workspace was present but RBAC object was invalid: %w", err)
69+
}
70+
}
71+
return rbacCtx, nil
72+
}

coderd/database/dbauthz/dbauthz.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,6 +2455,18 @@ func (q *querier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Contex
24552455
}
24562456

24572457
func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
2458+
// Fast path: Check if we have a workspace RBAC object in context.
2459+
if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok {
2460+
// Errors here will result in falling back to GetWorkspaceByAgentID,
2461+
// in case the cached data is stale.
2462+
if err := q.authorizeContext(ctx, policy.ActionRead, rbacObj); err == nil {
2463+
return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
2464+
}
2465+
2466+
q.log.Debug(ctx, "fast path authorization failed for GetLatestWorkspaceBuildByWorkspaceID, using slow path",
2467+
slog.F("workspace_id", workspaceID))
2468+
}
2469+
24582470
if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil {
24592471
return database.WorkspaceBuild{}, err
24602472
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4731,3 +4731,77 @@ func (s *MethodTestSuite) TestTelemetry() {
47314731
check.Args(database.CalculateAIBridgeInterceptionsTelemetrySummaryParams{}).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead)
47324732
}))
47334733
}
4734+
4735+
func TestGetLatestWorkspaceBuildByWorkspaceID_FastPath(t *testing.T) {
4736+
t.Parallel()
4737+
4738+
ownerID := uuid.New()
4739+
wsID := uuid.New()
4740+
orgID := uuid.New()
4741+
4742+
workspace := database.Workspace{
4743+
ID: wsID,
4744+
OwnerID: ownerID,
4745+
OrganizationID: orgID,
4746+
}
4747+
4748+
build := database.WorkspaceBuild{
4749+
ID: uuid.New(),
4750+
WorkspaceID: wsID,
4751+
}
4752+
4753+
wsIdentity := database.WorkspaceIdentity{
4754+
ID: wsID,
4755+
OwnerID: ownerID,
4756+
OrganizationID: orgID,
4757+
}
4758+
4759+
actor := rbac.Subject{
4760+
ID: ownerID.String(),
4761+
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
4762+
Groups: []string{orgID.String()},
4763+
Scope: rbac.ScopeAll,
4764+
}
4765+
4766+
authorizer := &coderdtest.RecordingAuthorizer{
4767+
Wrapped: (&coderdtest.FakeAuthorizer{}).AlwaysReturn(nil),
4768+
}
4769+
4770+
t.Run("WithWorkspaceRBAC", func(t *testing.T) {
4771+
t.Parallel()
4772+
4773+
ctx := dbauthz.As(context.Background(), actor)
4774+
ctrl := gomock.NewController(t)
4775+
dbm := dbmock.NewMockStore(ctrl)
4776+
4777+
rbacObj := wsIdentity.RBACObject()
4778+
ctx, err := dbauthz.WithWorkspaceRBAC(ctx, rbacObj)
4779+
require.NoError(t, err)
4780+
4781+
dbm.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil).AnyTimes()
4782+
dbm.EXPECT().Wrappers().Return([]string{})
4783+
4784+
q := dbauthz.New(dbm, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
4785+
4786+
result, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
4787+
require.NoError(t, err)
4788+
require.Equal(t, build, result)
4789+
})
4790+
t.Run("WithoutWorkspaceRBAC", func(t *testing.T) {
4791+
t.Parallel()
4792+
4793+
ctx := dbauthz.As(context.Background(), actor)
4794+
ctrl := gomock.NewController(t)
4795+
dbm := dbmock.NewMockStore(ctrl)
4796+
4797+
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), wsID).Return(workspace, nil).AnyTimes()
4798+
dbm.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil).AnyTimes()
4799+
dbm.EXPECT().Wrappers().Return([]string{})
4800+
4801+
q := dbauthz.New(dbm, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
4802+
4803+
result, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
4804+
require.NoError(t, err)
4805+
require.Equal(t, build, result)
4806+
})
4807+
}

coderd/workspaceagentsrpc.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,11 @@ func (api *API) startAgentYamuxMonitor(ctx context.Context,
227227
mux *yamux.Session,
228228
) *agentConnectionMonitor {
229229
monitor := &agentConnectionMonitor{
230-
apiCtx: api.ctx,
231-
workspace: workspace,
232-
workspaceAgent: workspaceAgent,
233-
workspaceBuild: workspaceBuild,
230+
apiCtx: api.ctx,
231+
workspace: workspace,
232+
workspaceAgent: workspaceAgent,
233+
workspaceBuild: workspaceBuild,
234+
234235
conn: &yamuxPingerCloser{mux: mux},
235236
pingPeriod: api.AgentConnectionUpdateFrequency,
236237
db: api.Database,
@@ -453,6 +454,13 @@ func (m *agentConnectionMonitor) monitor(ctx context.Context) {
453454
AgentID: &m.workspaceAgent.ID,
454455
})
455456
}
457+
458+
ctx, err := dbauthz.WithWorkspaceRBAC(ctx, m.workspace.RBACObject())
459+
if err != nil {
460+
// Don't error level log here, will exit the function. We want to fall back to GetWorkspaceByAgentID.
461+
//nolint:gocritic
462+
m.logger.Debug(ctx, "Cached workspace was present but RBAC object was invalid", slog.F("err", err))
463+
}
456464
err = checkBuildIsLatest(ctx, m.db, m.workspaceBuild)
457465
if err != nil {
458466
reason = err.Error()

0 commit comments

Comments
 (0)