Skip to content
28 changes: 28 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -3239,6 +3239,34 @@ func (q *querier) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.C
return file, nil
}

func (q *querier) GetChatFileDataPrefixesByIDs(ctx context.Context, arg database.GetChatFileDataPrefixesByIDsParams) ([]database.GetChatFileDataPrefixesByIDsRow, error) {
rows, err := q.db.GetChatFileDataPrefixesByIDs(ctx, arg)
if err != nil {
return nil, err
}
var prepared rbac.PreparedAuthorized
for _, row := range rows {
fileAuthErr := q.authorizeContext(ctx, policy.ActionRead, row)
if fileAuthErr == nil {
continue
}
if prepared == nil {
prepared, err = prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceChat.Type)
if err != nil {
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
}
}
chats, err := q.db.GetAuthorizedChatsByChatFileID(ctx, row.ID, prepared)
if err != nil {
return nil, err
}
if len(chats) == 0 {
return nil, fileAuthErr
}
}
return rows, nil
}

func (q *querier) GetChatFileMetadataByChatID(ctx context.Context, chatID uuid.UUID) ([]database.GetChatFileMetadataByChatIDRow, error) {
if _, err := q.GetChatByID(ctx, chatID); err != nil {
return nil, err
Expand Down
26 changes: 26 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@ func TestChatFilesAllowLinkedChatReads(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []database.ChatFile{file}, got)
})

t.Run("GetChatFileDataPrefixesByIDs", func(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
row := testutil.Fake(t, gofakeit.New(0), database.GetChatFileDataPrefixesByIDsRow{})
arg := database.GetChatFileDataPrefixesByIDsParams{IDs: []uuid.UUID{row.ID}, PrefixBytes: 64}

db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
db.EXPECT().GetChatFileDataPrefixesByIDs(gomock.Any(), arg).Return([]database.GetChatFileDataPrefixesByIDsRow{row}, nil)
db.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), row.ID, gomock.Any()).Return([]database.Chat{{ID: uuid.New()}}, nil)

q := dbauthz.New(db, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
got, err := q.GetChatFileDataPrefixesByIDs(ctx, arg)

require.NoError(t, err)
require.Equal(t, []database.GetChatFileDataPrefixesByIDsRow{row}, got)
})
}

//nolint:tparallel,paralleltest // It toggles the global chat ACL flag.
Expand Down Expand Up @@ -959,6 +978,13 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), file.ID, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
check.Args([]uuid.UUID{file.ID}).Asserts(rbac.ResourceChat.WithOwner(file.OwnerID.String()).InOrg(file.OrganizationID).WithID(file.ID), policy.ActionRead).Returns([]database.ChatFile{file})
}))
s.Run("GetChatFileDataPrefixesByIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
row := testutil.Fake(s.T(), faker, database.GetChatFileDataPrefixesByIDsRow{})
arg := database.GetChatFileDataPrefixesByIDsParams{IDs: []uuid.UUID{row.ID}, PrefixBytes: 64}
dbm.EXPECT().GetChatFileDataPrefixesByIDs(gomock.Any(), arg).Return([]database.GetChatFileDataPrefixesByIDsRow{row}, nil).AnyTimes()
dbm.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), row.ID, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(row.OwnerID.String()).InOrg(row.OrganizationID).WithID(row.ID), policy.ActionRead).Returns([]database.GetChatFileDataPrefixesByIDsRow{row})
}))
s.Run("GetChatFileMetadataByChatID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
file := testutil.Fake(s.T(), faker, database.ChatFile{})
Expand Down
8 changes: 8 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ func (c GetChatFileMetadataByChatIDRow) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}

func (c GetChatFileDataPrefixesByIDsRow) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}

func (s APIKeyScope) ToRBAC() rbac.ScopeName {
switch s {
case ApiKeyScopeCoderAll:
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package database_test

import (
"bytes"
"context"
"database/sql"
"encoding/json"
Expand Down Expand Up @@ -1918,6 +1919,56 @@ func TestGetAuthorizedChatsByChatFileIDACLSharing(t *testing.T) {
require.Empty(t, rows[0].GroupACL)
}

func TestGetChatFileDataPrefixesByIDs(t *testing.T) {
t.Parallel()
if testing.Short() {
t.SkipNow()
}

ctx := testutil.Context(t, testutil.WaitMedium)
sqlDB := testSQLDB(t)
err := migrations.Up(sqlDB)
require.NoError(t, err)
db := database.New(sqlDB)

owner := dbgen.User(t, db, database.User{})
org := dbgen.Organization(t, db, database.Organization{})

longData := bytes.Repeat([]byte("a"), 100)
longFile, err := db.InsertChatFile(ctx, database.InsertChatFileParams{
OwnerID: owner.ID,
OrganizationID: org.ID,
Name: "long.txt",
Mimetype: "text/plain",
Data: longData,
})
require.NoError(t, err)
shortFile, err := db.InsertChatFile(ctx, database.InsertChatFileParams{
OwnerID: owner.ID,
OrganizationID: org.ID,
Name: "short.txt",
Mimetype: "text/plain",
Data: []byte("tiny"),
})
require.NoError(t, err)

rows, err := db.GetChatFileDataPrefixesByIDs(ctx, database.GetChatFileDataPrefixesByIDsParams{
IDs: []uuid.UUID{longFile.ID, shortFile.ID},
PrefixBytes: 16,
})
require.NoError(t, err)
require.Len(t, rows, 2)

prefixes := make(map[uuid.UUID]database.GetChatFileDataPrefixesByIDsRow, len(rows))
for _, row := range rows {
prefixes[row.ID] = row
}
require.Equal(t, longData[:16], prefixes[longFile.ID].DataPrefix)
require.Equal(t, []byte("tiny"), prefixes[shortFile.ID].DataPrefix)
require.Equal(t, owner.ID, prefixes[longFile.ID].OwnerID)
require.Equal(t, org.ID, prefixes[longFile.ID].OrganizationID)
}

func TestInsertWorkspaceAgentLogs(t *testing.T) {
t.Parallel()
if testing.Short() {
Expand Down
49 changes: 49 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions coderd/database/queries/chatfiles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ SELECT * FROM chat_files WHERE id = @id::uuid;
-- name: GetChatFilesByIDs :many
SELECT * FROM chat_files WHERE id = ANY(@ids::uuid[]);

-- name: GetChatFileDataPrefixesByIDs :many
-- GetChatFileDataPrefixesByIDs returns a bounded prefix of each
-- file's content, keeping full blobs out of server memory. Owner and
-- organization columns support row-level authorization.
SELECT id, owner_id, organization_id, substr(data, 1, @prefix_bytes::int) AS data_prefix
FROM chat_files
WHERE id = ANY(@ids::uuid[]);

-- name: GetChatFileMetadataByChatID :many
-- GetChatFileMetadataByChatID returns lightweight file metadata for
-- all files linked to a chat. The data column is excluded to avoid
Expand Down
Loading
Loading