diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 4962949f7fd49..2579978273302 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3940,6 +3940,24 @@ func (q *querier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, return q.db.GetWorkspaceResourceMetadataCreatedAfter(ctx, createdAt) } +// GetWorkspaceResourceWithJobByID is an optimized version that fetches both +// the resource and job information in a single query. This is specifically +// designed for system-restricted contexts (like agent authentication) where +// we can bypass the authorization cascade that would normally call +// GetWorkspaceBuildByJobID. +func (q *querier) GetWorkspaceResourceWithJobByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceResourceWithJobByIDRow, error) { + // Authorize for system resource access. This will only succeed for + // system-restricted contexts, ensuring this optimized path is only used + // in appropriate scenarios. + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.GetWorkspaceResourceWithJobByIDRow{}, err + } + + // With system-restricted context, we can safely bypass the authorization + // cascade and call the database directly. + return q.db.GetWorkspaceResourceWithJobByID(ctx, id) +} + func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { job, err := q.db.GetProvisionerJobByID(ctx, jobID) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 11909b1a65e93..ab213f6b00c79 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1990,6 +1990,26 @@ func (s *MethodTestSuite) TestWorkspace() { dbm.EXPECT().GetWorkspaceByID(gomock.Any(), build.WorkspaceID).Return(ws, nil).AnyTimes() check.Args(res.ID).Asserts(ws, policy.ActionRead).Returns(res) })) + s.Run("GetWorkspaceResourceWithJobByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + res := testutil.Fake(s.T(), faker, database.WorkspaceResource{}) + resWithJob := database.GetWorkspaceResourceWithJobByIDRow{ + ID: res.ID, + CreatedAt: res.CreatedAt, + JobID: res.JobID, + Transition: res.Transition, + Type: res.Type, + Name: res.Name, + Hide: res.Hide, + Icon: res.Icon, + InstanceType: res.InstanceType, + DailyCost: res.DailyCost, + ModulePath: res.ModulePath, + JobType: database.ProvisionerJobTypeWorkspaceBuild, + JobInput: []byte(`{}`), + } + dbm.EXPECT().GetWorkspaceResourceWithJobByID(gomock.Any(), res.ID).Return(resWithJob, nil).AnyTimes() + check.Args(res.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(resWithJob) + })) s.Run("Build/GetWorkspaceResourcesByJobID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { ws := testutil.Fake(s.T(), faker, database.Workspace{}) build := testutil.Fake(s.T(), faker, database.WorkspaceBuild{WorkspaceID: ws.ID}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 6a018f41905f1..b605a8fe16005 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2258,6 +2258,13 @@ func (m queryMetricsStore) GetWorkspaceResourceMetadataCreatedAfter(ctx context. return metadata, err } +func (m queryMetricsStore) GetWorkspaceResourceWithJobByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceResourceWithJobByIDRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceResourceWithJobByID(ctx, id) + m.queryLatencies.WithLabelValues("GetWorkspaceResourceWithJobByID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { start := time.Now() resources, err := m.s.GetWorkspaceResourcesByJobID(ctx, jobID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index f25e91e90c249..7ce4f971967db 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4828,6 +4828,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceResourceMetadataCreatedAfter(ctx, c return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourceMetadataCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourceMetadataCreatedAfter), ctx, createdAt) } +// GetWorkspaceResourceWithJobByID mocks base method. +func (m *MockStore) GetWorkspaceResourceWithJobByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceResourceWithJobByIDRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceResourceWithJobByID", ctx, id) + ret0, _ := ret[0].(database.GetWorkspaceResourceWithJobByIDRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceResourceWithJobByID indicates an expected call of GetWorkspaceResourceWithJobByID. +func (mr *MockStoreMockRecorder) GetWorkspaceResourceWithJobByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourceWithJobByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourceWithJobByID), ctx, id) +} + // GetWorkspaceResourcesByJobID mocks base method. func (m *MockStore) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7202d22f3d142..c8c6a9d2ed87c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -521,6 +521,7 @@ type sqlcQuerier interface { GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) + GetWorkspaceResourceWithJobByID(ctx context.Context, id uuid.UUID) (GetWorkspaceResourceWithJobByIDRow, error) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c803d569365ca..c6727a8ff9c31 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -21815,6 +21815,66 @@ func (q *sqlQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Contex return items, nil } +const getWorkspaceResourceWithJobByID = `-- name: GetWorkspaceResourceWithJobByID :one +SELECT + wr.id, + wr.created_at, + wr.job_id, + wr.transition, + wr.type, + wr.name, + wr.hide, + wr.icon, + wr.instance_type, + wr.daily_cost, + wr.module_path, + pj.type AS job_type, + pj.input AS job_input +FROM + workspace_resources wr +JOIN + provisioner_jobs pj ON wr.job_id = pj.id +WHERE + wr.id = $1 +` + +type GetWorkspaceResourceWithJobByIDRow struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + Type string `db:"type" json:"type"` + Name string `db:"name" json:"name"` + Hide bool `db:"hide" json:"hide"` + Icon string `db:"icon" json:"icon"` + InstanceType sql.NullString `db:"instance_type" json:"instance_type"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` + ModulePath sql.NullString `db:"module_path" json:"module_path"` + JobType ProvisionerJobType `db:"job_type" json:"job_type"` + JobInput json.RawMessage `db:"job_input" json:"job_input"` +} + +func (q *sqlQuerier) GetWorkspaceResourceWithJobByID(ctx context.Context, id uuid.UUID) (GetWorkspaceResourceWithJobByIDRow, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceResourceWithJobByID, id) + var i GetWorkspaceResourceWithJobByIDRow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.JobID, + &i.Transition, + &i.Type, + &i.Name, + &i.Hide, + &i.Icon, + &i.InstanceType, + &i.DailyCost, + &i.ModulePath, + &i.JobType, + &i.JobInput, + ) + return i, err +} + const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path diff --git a/coderd/database/queries/workspaceresources.sql b/coderd/database/queries/workspaceresources.sql index 63fb9a26374a8..1922be4360642 100644 --- a/coderd/database/queries/workspaceresources.sql +++ b/coderd/database/queries/workspaceresources.sql @@ -52,3 +52,25 @@ SELECT SELECT * FROM workspace_resource_metadata WHERE workspace_resource_id = ANY( SELECT id FROM workspace_resources WHERE created_at > $1 ); + +-- name: GetWorkspaceResourceWithJobByID :one +SELECT + wr.id, + wr.created_at, + wr.job_id, + wr.transition, + wr.type, + wr.name, + wr.hide, + wr.icon, + wr.instance_type, + wr.daily_cost, + wr.module_path, + pj.type AS job_type, + pj.input AS job_input +FROM + workspace_resources wr +JOIN + provisioner_jobs pj ON wr.job_id = pj.id +WHERE + wr.id = $1; diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go index 3642822b18d77..1c416c735f054 100644 --- a/coderd/workspaceresourceauth.go +++ b/coderd/workspaceresourceauth.go @@ -143,7 +143,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in return } //nolint:gocritic // needed for auth instance id - resource, err := api.Database.GetWorkspaceResourceByID(dbauthz.AsSystemRestricted(ctx), agent.ResourceID) + resourceWithJob, err := api.Database.GetWorkspaceResourceWithJobByID(dbauthz.AsSystemRestricted(ctx), agent.ResourceID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching provisioner job resource.", @@ -151,23 +151,14 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in }) return } - //nolint:gocritic // needed for auth instance id - job, err := api.Database.GetProvisionerJobByID(dbauthz.AsSystemRestricted(ctx), resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner job.", - Detail: err.Error(), - }) - return - } - if job.Type != database.ProvisionerJobTypeWorkspaceBuild { + if resourceWithJob.JobType != database.ProvisionerJobTypeWorkspaceBuild { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("%q jobs cannot be authenticated.", job.Type), + Message: fmt.Sprintf("%q jobs cannot be authenticated.", resourceWithJob.JobType), }) return } var jobData provisionerdserver.WorkspaceProvisionJob - err = json.Unmarshal(job.Input, &jobData) + err = json.Unmarshal(resourceWithJob.JobInput, &jobData) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error extracting job data.",