From e0a650f62819541d63273febce3dc7c9df0d1b15 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Feb 2023 15:52:43 -0600 Subject: [PATCH 1/4] chore: Skip authz on various functions used for api data building API already fetches the parent object and does the rbac check. Until these functions are optimized, skipping authz is better. It leaves us no worse off than the status quo --- coderd/database/dbauthz/querier.go | 107 +---------------------------- coderd/database/dbauthz/system.go | 50 ++++++++++++++ coderd/templates.go | 11 +-- coderd/workspaces.go | 3 +- 4 files changed, 59 insertions(+), 112 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 691f680e42feb..b89711d3d0e07 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -251,11 +251,7 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data return job, nil } -func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { - // TODO: This is missing authorization and is incorrect. This call is used by telemetry, and by 1 http route. - // That http handler should find a better way to fetch these jobs with easier rbac authz. - return q.db.GetProvisionerJobsByIDs(ctx, ids) -} + func (q *querier) GetProvisionerLogsByIDBetween(ctx context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) { // Authorized read on job lets the actor also read the logs. @@ -725,34 +721,6 @@ func (q *querier) GetTemplateVersionVariables(ctx context.Context, templateVersi return q.db.GetTemplateVersionVariables(ctx, templateVersionID) } -func (q *querier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { - // TODO: This is so inefficient - versions, err := q.db.GetTemplateVersionsByIDs(ctx, ids) - if err != nil { - return nil, err - } - checked := make(map[uuid.UUID]bool) - for _, v := range versions { - if _, ok := checked[v.TemplateID.UUID]; ok { - continue - } - - obj := v.RBACObjectNoTemplate() - template, err := q.db.GetTemplateByID(ctx, v.TemplateID.UUID) - if err == nil { - obj = v.RBACObject(template) - } - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - return nil, err - } - if err := q.authorizeContext(ctx, rbac.ActionRead, obj); err != nil { - return nil, err - } - checked[v.TemplateID.UUID] = true - } - - return versions, nil -} func (q *querier) GetTemplateVersionsByTemplateID(ctx context.Context, arg database.GetTemplateVersionsByTemplateIDParams) ([]database.TemplateVersion, error) { // An actor can read template versions if they can read the related template. @@ -1013,11 +981,6 @@ func (q *querier) GetUsersWithCount(ctx context.Context, arg database.GetUsersPa return users, rowUsers[0].Count, nil } -// TODO: Remove this and use a filter on GetUsers -func (q *querier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]database.User, error) { - return fetchWithPostFilter(q.auth, q.db.GetUsersByIDs)(ctx, ids) -} - func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { // Always check if the assigned roles can actually be assigned by this actor. impliedRoles := append([]string{rbac.RoleMember()}, arg.RBACRoles...) @@ -1222,36 +1185,7 @@ func (q *querier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanc return agent, nil } -// GetWorkspaceAgentsByResourceIDs is an all or nothing call. If the user cannot read -// a single agent, the entire call will fail. -func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { - if _, ok := ActorFromContext(ctx); !ok { - return nil, NoActorError - } - // TODO: Make this more efficient. This is annoying because all these resources should be owned by the same workspace. - // So the authz check should just be 1 check, but we cannot do that easily here. We should see if all callers can - // instead do something like GetWorkspaceAgentsByWorkspaceID. - agents, err := q.db.GetWorkspaceAgentsByResourceIDs(ctx, ids) - if err != nil { - return nil, err - } - for _, a := range agents { - // Check if we can fetch the workspace by the agent ID. - _, err := q.GetWorkspaceByAgentID(ctx, a.ID) - if err == nil { - continue - } - if errors.Is(err, sql.ErrNoRows) && !errors.As(err, &NotAuthorizedError{}) { - // The agent is not tied to a workspace, likely from an orphaned template version. - // Just return it. - continue - } - // Otherwise, we cannot read the workspace, so we cannot read the agent. - return nil, err - } - return agents, nil -} func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error { agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID) @@ -1305,19 +1239,7 @@ func (q *querier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UU return q.db.GetWorkspaceAppsByAgentID(ctx, agentID) } -// GetWorkspaceAppsByAgentIDs is an all or nothing call. If the user cannot read a single app, the entire call will fail. -func (q *querier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { - // TODO: This should be reworked. All these apps are likely owned by the same workspace, so we should be able to - // do 1 authz call. We should refactor this to be GetWorkspaceAppsByWorkspaceID. - for _, id := range ids { - _, err := q.GetWorkspaceAgentByID(ctx, id) - if err != nil { - return nil, err - } - } - return q.db.GetWorkspaceAppsByAgentIDs(ctx, ids) -} func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuild, error) { build, err := q.db.GetWorkspaceBuildByID(ctx, buildID) @@ -1395,20 +1317,6 @@ func (q *querier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (d return resource, nil } -// GetWorkspaceResourceMetadataByResourceIDs is an all or nothing call. If a single resource is not authorized, then -// an error is returned. -func (q *querier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) { - // TODO: This is very inefficient. Since all these resources are likely asscoiated with the same workspace. - for _, id := range ids { - // If we can read the resource, we can read the metadata. - _, err := q.GetWorkspaceResourceByID(ctx, id) - if err != nil { - return nil, err - } - } - - return q.db.GetWorkspaceResourceMetadataByResourceIDs(ctx, ids) -} func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { job, err := q.db.GetProvisionerJobByID(ctx, jobID) @@ -1455,20 +1363,7 @@ func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.U return q.db.GetWorkspaceResourcesByJobID(ctx, jobID) } -// GetWorkspaceResourcesByJobIDs is an all or nothing call. If a single resource is not authorized, then -// an error is returned. -func (q *querier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResource, error) { - // TODO: This is very inefficient. Since all these resources are likely asscoiated with the same workspace. - for _, id := range ids { - // If we can read the resource, we can read the metadata. - _, err := q.GetProvisionerJobByID(ctx, id) - if err != nil { - return nil, err - } - } - return q.db.GetWorkspaceResourcesByJobIDs(ctx, ids) -} func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) { obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index 5baf6ad7604eb..674eae05de33f 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -14,6 +14,56 @@ import ( // to these objects. Might need a negative permission on the `Owner` role to // prevent owners. +// GetWorkspaceAppsByAgentIDs +// The workspace/job is already fetched. +// TODO: This function should be removed/replaced with something with proper auth. +func (q *querier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { + return q.db.GetWorkspaceAppsByAgentIDs(ctx, ids) +} + +// GetWorkspaceAgentsByResourceIDs +// The workspace/job is already fetched. +// TODO: This function should be removed/replaced with something with proper auth. +func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { + return q.db.GetWorkspaceAgentsByResourceIDs(ctx, ids) +} + +// GetWorkspaceResourceMetadataByResourceIDs is only used for build data. +// The workspace/job is already fetched. +// TODO: This function should be removed/replaced with something with proper auth. +func (q *querier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) { + return q.db.GetWorkspaceResourceMetadataByResourceIDs(ctx, ids) +} + +// GetUsersByIDs is only used for usernames on workspace return data. +// This function should be replaced by joining this data to the workspace query +// itself. +// TODO: This function should be removed/replaced with something with proper auth. +// A SQL compiled filter is an option. +func (q *querier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]database.User, error) { + return q.db.GetUsersByIDs(ctx, ids) +} + +func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { + // TODO: This is missing authorization and is incorrect. This call is used by telemetry, and by 1 http route. + // That http handler should find a better way to fetch these jobs with easier rbac authz. + return q.db.GetProvisionerJobsByIDs(ctx, ids) +} + +// GetTemplateVersionsByIDs is only used for workspace build data. +// The workspace is already fetched. +// TODO: Find a way to replace this with proper authz. +func (q *querier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { + return q.GetTemplateVersionsByIDs(ctx, ids) +} + +// GetWorkspaceResourcesByJobIDs is only used for workspace build data. +// The workspace is already fetched. +// TODO: Find a way to replace this with proper authz. +func (q *querier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResource, error) { + return q.db.GetWorkspaceResourcesByJobIDs(ctx, ids) +} + func (q *querier) UpdateUserLinkedID(ctx context.Context, arg database.UpdateUserLinkedIDParams) (database.UserLink, error) { return q.db.UpdateUserLinkedID(ctx, arg) } diff --git a/coderd/templates.go b/coderd/templates.go index 564ccd74946f5..cd7e31683842f 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -9,6 +9,8 @@ import ( "sort" "time" + "github.com/coder/coder/coderd/database/dbauthz" + "github.com/go-chi/chi/v5" "github.com/google/uuid" "golang.org/x/xerrors" @@ -82,11 +84,10 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) { return } - // TODO: This just returns the workspaces a user can view. We should use - // a system function to get all workspaces that use this template. - // This data should never be exposed to the user aside from a non-zero count. - // Or we move this into a postgres constraint. - workspaces, err := api.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{ + // This is just to get the workspace count, so we use a system context to + // return ALL workspaces. Not just workspaces the user can view. + // nolint:gocritic + workspaces, err := api.Database.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{ TemplateIds: []uuid.UUID{template.ID}, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 5e7398da29f23..4bafa506b42aa 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -570,7 +570,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } aReq.New = workspace - users, err := api.Database.GetUsersByIDs(ctx, []uuid.UUID{user.ID, workspaceBuild.InitiatorID}) + initiator, err := api.Database.GetUserByID(ctx, workspaceBuild.InitiatorID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching user.", @@ -584,6 +584,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(workspaceBuild)}, }) + users := []database.User{user, initiator} apiBuild, err := api.convertWorkspaceBuild( workspaceBuild, workspace, From 28c87432dc5cb0f9cdfecc9d56007f02ee9780db Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Feb 2023 16:06:15 -0600 Subject: [PATCH 2/4] Fix import order --- coderd/templates.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index cd7e31683842f..c361d02417d4a 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -9,14 +9,13 @@ import ( "sort" "time" - "github.com/coder/coder/coderd/database/dbauthz" - "github.com/go-chi/chi/v5" "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" From 3458b05859644ec0f94698f82970d803dd979bef Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Feb 2023 20:25:46 -0600 Subject: [PATCH 3/4] fix recursion --- coderd/database/dbauthz/system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index 674eae05de33f..cc0e99a5e3d8f 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -54,7 +54,7 @@ func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) // The workspace is already fetched. // TODO: Find a way to replace this with proper authz. func (q *querier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { - return q.GetTemplateVersionsByIDs(ctx, ids) + return q.db.GetTemplateVersionsByIDs(ctx, ids) } // GetWorkspaceResourcesByJobIDs is only used for workspace build data. From f7c5d05c31990fc112abf1a18c6afbecd6c8b1d3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Feb 2023 23:02:21 -0600 Subject: [PATCH 4/4] Make fmt --- coderd/database/dbauthz/querier.go | 10 ---------- coderd/database/dbauthz/querier_test.go | 14 ++++++++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index b89711d3d0e07..f24a04af8845c 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -251,8 +251,6 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data return job, nil } - - func (q *querier) GetProvisionerLogsByIDBetween(ctx context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) { // Authorized read on job lets the actor also read the logs. _, err := q.GetProvisionerJobByID(ctx, arg.JobID) @@ -721,7 +719,6 @@ func (q *querier) GetTemplateVersionVariables(ctx context.Context, templateVersi return q.db.GetTemplateVersionVariables(ctx, templateVersionID) } - func (q *querier) GetTemplateVersionsByTemplateID(ctx context.Context, arg database.GetTemplateVersionsByTemplateIDParams) ([]database.TemplateVersion, error) { // An actor can read template versions if they can read the related template. template, err := q.db.GetTemplateByID(ctx, arg.TemplateID) @@ -1185,8 +1182,6 @@ func (q *querier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanc return agent, nil } - - func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error { agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID) if err != nil { @@ -1239,8 +1234,6 @@ func (q *querier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UU return q.db.GetWorkspaceAppsByAgentID(ctx, agentID) } - - func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuild, error) { build, err := q.db.GetWorkspaceBuildByID(ctx, buildID) if err != nil { @@ -1317,7 +1310,6 @@ func (q *querier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (d return resource, nil } - func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { job, err := q.db.GetProvisionerJobByID(ctx, jobID) if err != nil { @@ -1363,8 +1355,6 @@ func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.U return q.db.GetWorkspaceResourcesByJobID(ctx, jobID) } - - func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) { obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg) diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index 0f7e7c4ffa45d..8c56af866f811 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -622,7 +622,7 @@ func (s *MethodTestSuite) TestTemplate() { TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, }) check.Args([]uuid.UUID{tv1.ID, tv2.ID, tv3.ID}). - Asserts(t1, rbac.ActionRead, t2, rbac.ActionRead). + Asserts( /*t1, rbac.ActionRead, t2, rbac.ActionRead*/ ). Returns(slice.New(tv1, tv2, tv3)) })) s.Run("GetTemplateVersionsByTemplateID", s.Subtest(func(db database.Store, check *expects) { @@ -797,7 +797,7 @@ func (s *MethodTestSuite) TestUser() { a := dbgen.User(s.T(), db, database.User{CreatedAt: database.Now().Add(-time.Hour)}) b := dbgen.User(s.T(), db, database.User{CreatedAt: database.Now()}) check.Args([]uuid.UUID{a.ID, b.ID}). - Asserts(a, rbac.ActionRead, b, rbac.ActionRead). + Asserts( /*a, rbac.ActionRead, b, rbac.ActionRead*/ ). Returns(slice.New(a, b)) })) s.Run("InsertUser", s.Subtest(func(db database.Store, check *expects) { @@ -972,7 +972,7 @@ func (s *MethodTestSuite) TestWorkspace() { build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - check.Args([]uuid.UUID{res.ID}).Asserts(ws, rbac.ActionRead). + check.Args([]uuid.UUID{res.ID}).Asserts( /*ws, rbac.ActionRead*/ ). Returns([]database.WorkspaceAgent{agt}) })) s.Run("UpdateWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { @@ -1030,7 +1030,7 @@ func (s *MethodTestSuite) TestWorkspace() { b := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: bAgt.ID}) check.Args([]uuid.UUID{a.AgentID, b.AgentID}). - Asserts(aWs, rbac.ActionRead, bWs, rbac.ActionRead). + Asserts( /*aWs, rbac.ActionRead, bWs, rbac.ActionRead*/ ). Returns([]database.WorkspaceApp{a, b}) })) s.Run("GetWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { @@ -1093,7 +1093,7 @@ func (s *MethodTestSuite) TestWorkspace() { a := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) b := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) check.Args([]uuid.UUID{a.ID, b.ID}). - Asserts(ws, []rbac.Action{rbac.ActionRead, rbac.ActionRead}) + Asserts( /*ws, []rbac.Action{rbac.ActionRead, rbac.ActionRead}*/ ) })) s.Run("Build/GetWorkspaceResourcesByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) @@ -1115,7 +1115,9 @@ func (s *MethodTestSuite) TestWorkspace() { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) wJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) - check.Args([]uuid.UUID{tJob.ID, wJob.ID}).Asserts(v.RBACObject(tpl), rbac.ActionRead, ws, rbac.ActionRead).Returns([]database.WorkspaceResource{}) + check.Args([]uuid.UUID{tJob.ID, wJob.ID}). + Asserts( /*v.RBACObject(tpl), rbac.ActionRead, ws, rbac.ActionRead*/ ). + Returns([]database.WorkspaceResource{}) })) s.Run("InsertWorkspace", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{})