From 3fbbaa7bf67e21b198cc905a449654729a5e97ae Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 Mar 2023 11:35:56 -0500 Subject: [PATCH 1/4] fix: Users that can update a template can also read the file This currently has a strange RBAC story. An issue will be filed to streamline this. This is a hotfix to resolve current functionality --- coderd/database/dbauthz/querier.go | 50 ++++++++++++++++++- coderd/database/dbauthz/system.go | 7 +++ coderd/database/dbfake/databasefake.go | 41 +++++++++++++++ coderd/database/modelmethods.go | 7 +++ coderd/database/querier.go | 2 + coderd/database/queries.sql.go | 69 ++++++++++++++++++++++++++ coderd/database/queries/files.sql | 28 +++++++++++ 7 files changed, 202 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 2ee0579281038..a2d850f7ddcb5 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -88,11 +88,57 @@ func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditL } func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { - return fetch(q.log, q.auth, q.db.GetFileByHashAndCreator)(ctx, arg) + file, err := q.db.GetFileByHashAndCreator(ctx, arg) + if err != nil { + return database.File{}, err + } + err = q.authorizeContext(ctx, rbac.ActionRead, file) + if err != nil { + // Check the user's access to the file's templates. + if q.authorizeReadFile(ctx, file) != nil { + return database.File{}, err + } + } + + return file, nil } func (q *querier) GetFileByID(ctx context.Context, id uuid.UUID) (database.File, error) { - return fetch(q.log, q.auth, q.db.GetFileByID)(ctx, id) + file, err := q.db.GetFileByID(ctx, id) + if err != nil { + return database.File{}, err + } + err = q.authorizeContext(ctx, rbac.ActionRead, file) + if err != nil { + // Check the user's access to the file's templates. + if q.authorizeUpdateFileTemplate(ctx, file) != nil { + return database.File{}, err + } + } + + return file, nil +} + +// authorizeReadFile is a hotfix for the fact that file permissions are +// independent of template permissions. This function checks if the user has +// update access to any of the file's templates. +func (q *querier) authorizeUpdateFileTemplate(ctx context.Context, file database.File) error { + tpls, err := q.GetFileTemplates(AsSystemRestricted(ctx), file.ID) + if err != nil { + return err + } + // There __should__ only be 1 template per file, but there can be more than + // 1, so check them all. + for _, tpl := range tpls { + // If the user has update access to any template, they have read access to the file. + if err := q.authorizeContext(ctx, rbac.ActionUpdate, tpl); err == nil { + return nil + } + } + + return NotAuthorizedError{ + Err: xerrors.Errorf("not authorized to read file %s", file.ID), + } } func (q *querier) InsertFile(ctx context.Context, arg database.InsertFileParams) (database.File, error) { diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index d27ff55ea0afe..28ca1628e00e1 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -10,6 +10,13 @@ import ( "github.com/coder/coder/coderd/rbac" ) +func (q *querier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]database.GetFileTemplatesRow, error) { + if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetFileTemplates(ctx, fileID) +} + // GetWorkspaceAppsByAgentIDs // The workspace/job is already fetched. func (q *querier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index a38b9baeacad7..f85cbadfd2e51 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -685,6 +685,47 @@ func (q *fakeQuerier) GetFileByID(_ context.Context, id uuid.UUID) (database.Fil return database.File{}, sql.ErrNoRows } +func (q *fakeQuerier) GetFileTemplates(_ context.Context, id uuid.UUID) ([]database.GetFileTemplatesRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + rows := make([]database.GetFileTemplatesRow, 0) + var file database.File + for _, f := range q.files { + if f.ID == id { + file = f + break + } + } + if file.Hash == "" { + return rows, nil + } + + for _, job := range q.provisionerJobs { + if job.FileID == id { + for _, version := range q.templateVersions { + if version.JobID == job.ID { + for _, template := range q.templates { + if template.ID == version.TemplateID.UUID { + rows = append(rows, database.GetFileTemplatesRow{ + FileID: file.ID, + FileCreatedBy: file.CreatedBy, + TemplateID: template.ID, + TemplateOrganizationID: template.OrganizationID, + TemplateCreatedBy: template.CreatedBy, + UserACL: template.UserACL, + GroupACL: template.GroupACL, + }) + } + } + } + } + } + } + + return rows, sql.ErrNoRows +} + func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { return database.User{}, err diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 870052afc8b75..0d037b7c509f9 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -88,6 +88,13 @@ func (t Template) RBACObject() rbac.Object { WithGroupACL(t.GroupACL) } +func (t GetFileTemplatesRow) RBACObject() rbac.Object { + return rbac.ResourceTemplate.WithID(t.TemplateID). + InOrg(t.TemplateOrganizationID). + WithACLUserList(t.UserACL). + WithGroupACL(t.GroupACL) +} + func (t Template) DeepCopy() Template { cpy := t cpy.UserACL = maps.Clone(t.UserACL) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 99cb9f7624301..793d3909c9ee6 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -60,6 +60,8 @@ type sqlcQuerier interface { GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) + // Get all templates that use a file. + GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) // This will never count deleted users. GetFilteredUserCount(ctx context.Context, arg GetFilteredUserCountParams) (int64, error) GetGitAuthLink(ctx context.Context, arg GetGitAuthLinkParams) (GitAuthLink, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index dd9fe69dd22ba..42912f8968b57 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -674,6 +674,75 @@ func (q *sqlQuerier) GetFileByID(ctx context.Context, id uuid.UUID) (File, error return i, err } +const getFileTemplates = `-- name: GetFileTemplates :many +SELECT + files.id AS file_id, + files.created_by AS file_created_by, + templates.id AS template_id, + templates.organization_id AS template_organization_id, + templates.created_by AS template_created_by, + templates.user_acl, + templates.group_acl +FROM + templates +INNER JOIN + template_versions + ON templates.id = template_versions.template_id +INNER JOIN + provisioner_jobs + ON job_id = provisioner_jobs.id +INNER JOIN + files + ON files.id = provisioner_jobs.file_id +WHERE + -- Only fetch template version associated files. + storage_method = 'file' + AND provisioner_jobs.type = 'template_version_import' + AND file_id = $1 +` + +type GetFileTemplatesRow struct { + FileID uuid.UUID `db:"file_id" json:"file_id"` + FileCreatedBy uuid.UUID `db:"file_created_by" json:"file_created_by"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"` + TemplateCreatedBy uuid.UUID `db:"template_created_by" json:"template_created_by"` + UserACL TemplateACL `db:"user_acl" json:"user_acl"` + GroupACL TemplateACL `db:"group_acl" json:"group_acl"` +} + +// Get all templates that use a file. +func (q *sqlQuerier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) { + rows, err := q.db.QueryContext(ctx, getFileTemplates, fileID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFileTemplatesRow + for rows.Next() { + var i GetFileTemplatesRow + if err := rows.Scan( + &i.FileID, + &i.FileCreatedBy, + &i.TemplateID, + &i.TemplateOrganizationID, + &i.TemplateCreatedBy, + &i.UserACL, + &i.GroupACL, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const insertFile = `-- name: InsertFile :one INSERT INTO files (id, hash, created_at, created_by, mimetype, "data") diff --git a/coderd/database/queries/files.sql b/coderd/database/queries/files.sql index 1f54386bb3ac4..97fded9a6353a 100644 --- a/coderd/database/queries/files.sql +++ b/coderd/database/queries/files.sql @@ -26,3 +26,31 @@ INSERT INTO files (id, hash, created_at, created_by, mimetype, "data") VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; + +-- name: GetFileTemplates :many +-- Get all templates that use a file. +SELECT + files.id AS file_id, + files.created_by AS file_created_by, + templates.id AS template_id, + templates.organization_id AS template_organization_id, + templates.created_by AS template_created_by, + templates.user_acl, + templates.group_acl +FROM + templates +INNER JOIN + template_versions + ON templates.id = template_versions.template_id +INNER JOIN + provisioner_jobs + ON job_id = provisioner_jobs.id +INNER JOIN + files + ON files.id = provisioner_jobs.file_id +WHERE + -- Only fetch template version associated files. + storage_method = 'file' + AND provisioner_jobs.type = 'template_version_import' + AND file_id = @file_id +; From 0b239845bc8f5c811361658f0a5d76fa81734d33 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 Mar 2023 11:55:23 -0500 Subject: [PATCH 2/4] Add unit test --- coderd/database/dbauthz/querier.go | 4 +-- coderd/database/dbfake/databasefake.go | 2 +- enterprise/coderd/templates_test.go | 48 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index a2d850f7ddcb5..b52034b19f874 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -95,7 +95,7 @@ func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetF err = q.authorizeContext(ctx, rbac.ActionRead, file) if err != nil { // Check the user's access to the file's templates. - if q.authorizeReadFile(ctx, file) != nil { + if q.authorizeUpdateFileTemplate(ctx, file) != nil { return database.File{}, err } } @@ -123,7 +123,7 @@ func (q *querier) GetFileByID(ctx context.Context, id uuid.UUID) (database.File, // independent of template permissions. This function checks if the user has // update access to any of the file's templates. func (q *querier) authorizeUpdateFileTemplate(ctx context.Context, file database.File) error { - tpls, err := q.GetFileTemplates(AsSystemRestricted(ctx), file.ID) + tpls, err := q.db.GetFileTemplates(ctx, file.ID) if err != nil { return err } diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index f85cbadfd2e51..7efd098957f70 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -723,7 +723,7 @@ func (q *fakeQuerier) GetFileTemplates(_ context.Context, id uuid.UUID) ([]datab } } - return rows, sql.ErrNoRows + return rows, nil } func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 65989da54383f..7ccc67854252c 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -976,6 +976,54 @@ func TestUpdateTemplateACL(t *testing.T) { }) } +func TestReadFileWithTemplateUpdate(t *testing.T) { + t.Parallel() + t.Run("HasTemplateUpdate", func(t *testing.T) { + t.Parallel() + ctx, cancel := testutil.Context(t) + defer cancel() + + // Upload a file + client := coderdenttest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }) + + resp, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(make([]byte, 1024))) + require.NoError(t, err) + + // Make a new user + member, memberData := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + // Try to download file, this should fail + _, _, err = member.Download(ctx, resp.ID) + require.Error(t, err, "no template yet") + + // Make a new template version with the file + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) { + request.FileID = resp.ID + }) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + + // Not in acl yet + _, _, err = member.Download(ctx, resp.ID) + require.Error(t, err, "not in acl yet") + + err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ + UserPerms: map[string]codersdk.TemplateRole{ + memberData.ID.String(): codersdk.TemplateRoleAdmin, + }, + }) + require.NoError(t, err) + + _, _, err = member.Download(ctx, resp.ID) + require.NoError(t, err) + }) +} + // TestTemplateAccess tests the rego -> sql conversion. We need to implement // this test on at least 1 table type to ensure that the conversion is correct. // The rbac tests only assert against static SQL queries. From 496575052d6079cd8519f937d1140ee1dca748e4 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 24 Mar 2023 17:42:57 +0000 Subject: [PATCH 3/4] Only showsource code tab if the user has permission to edit the template --- .../TemplateLayout/TemplateLayout.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index ed1ce35093e1f..c44d3ac5a9b6b 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -110,17 +110,19 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ > Summary - - combineClasses([ - styles.tabItem, - isActive ? styles.tabItemActive : undefined, - ]) - } - > - Source Code - + {data.permissions.canUpdateTemplate && ( + + combineClasses([ + styles.tabItem, + isActive ? styles.tabItemActive : undefined, + ]) + } + > + Source Code + + )} From b11fea2fd1a4892a64e36afb5f69ff16464f0da8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Mar 2023 08:14:07 -0500 Subject: [PATCH 4/4] Fix unit test from merge --- enterprise/coderd/templates_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index ef97590157b89..413662cbdedf6 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -980,8 +980,7 @@ func TestReadFileWithTemplateUpdate(t *testing.T) { t.Parallel() t.Run("HasTemplateUpdate", func(t *testing.T) { t.Parallel() - ctx, cancel := testutil.Context(t) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) // Upload a file client := coderdenttest.New(t, nil)