mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add chat sharing foundation (#25041)
This commit is contained in:
Generated
+5
@@ -14668,6 +14668,7 @@ const docTemplate = `{
|
||||
"chat:create",
|
||||
"chat:delete",
|
||||
"chat:read",
|
||||
"chat:share",
|
||||
"chat:update",
|
||||
"coder:all",
|
||||
"coder:apikeys.manage_self",
|
||||
@@ -14888,6 +14889,7 @@ const docTemplate = `{
|
||||
"APIKeyScopeChatCreate",
|
||||
"APIKeyScopeChatDelete",
|
||||
"APIKeyScopeChatRead",
|
||||
"APIKeyScopeChatShare",
|
||||
"APIKeyScopeChatUpdate",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApikeysManageSelf",
|
||||
@@ -17879,6 +17881,9 @@ const docTemplate = `{
|
||||
"derp": {
|
||||
"$ref": "#/definitions/codersdk.DERP"
|
||||
},
|
||||
"disable_chat_sharing": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_owner_workspace_exec": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
Generated
+5
@@ -13120,6 +13120,7 @@
|
||||
"chat:create",
|
||||
"chat:delete",
|
||||
"chat:read",
|
||||
"chat:share",
|
||||
"chat:update",
|
||||
"coder:all",
|
||||
"coder:apikeys.manage_self",
|
||||
@@ -13340,6 +13341,7 @@
|
||||
"APIKeyScopeChatCreate",
|
||||
"APIKeyScopeChatDelete",
|
||||
"APIKeyScopeChatRead",
|
||||
"APIKeyScopeChatShare",
|
||||
"APIKeyScopeChatUpdate",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApikeysManageSelf",
|
||||
@@ -16219,6 +16221,9 @@
|
||||
"derp": {
|
||||
"$ref": "#/definitions/codersdk.DERP"
|
||||
},
|
||||
"disable_chat_sharing": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_owner_workspace_exec": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+5
-1
@@ -343,16 +343,20 @@ func New(options *Options) *API {
|
||||
panic("developer error: options.PrometheusRegistry is nil and not running a unit test")
|
||||
}
|
||||
|
||||
if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing {
|
||||
if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing || options.DeploymentValues.DisableChatSharing {
|
||||
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
|
||||
NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
|
||||
NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing),
|
||||
NoChatSharing: bool(options.DeploymentValues.DisableChatSharing),
|
||||
})
|
||||
}
|
||||
|
||||
if options.DeploymentValues.DisableWorkspaceSharing {
|
||||
rbac.SetWorkspaceACLDisabled(true)
|
||||
}
|
||||
if options.DeploymentValues.DisableChatSharing {
|
||||
rbac.SetChatACLDisabled(true)
|
||||
}
|
||||
|
||||
if options.PrometheusRegistry == nil {
|
||||
options.PrometheusRegistry = prometheus.NewRegistry()
|
||||
|
||||
@@ -19,6 +19,9 @@ const (
|
||||
CheckChatUsageLimitConfigDefaultLimitMicrosCheck CheckConstraint = "chat_usage_limit_config_default_limit_micros_check" // chat_usage_limit_config
|
||||
CheckChatUsageLimitConfigPeriodCheck CheckConstraint = "chat_usage_limit_config_period_check" // chat_usage_limit_config
|
||||
CheckChatUsageLimitConfigSingletonCheck CheckConstraint = "chat_usage_limit_config_singleton_check" // chat_usage_limit_config
|
||||
CheckChatAclOnlyOnRootChats CheckConstraint = "chat_acl_only_on_root_chats" // chats
|
||||
CheckChatGroupAclNotNullJsonb CheckConstraint = "chat_group_acl_not_null_jsonb" // chats
|
||||
CheckChatUserAclNotNullJsonb CheckConstraint = "chat_user_acl_not_null_jsonb" // chats
|
||||
CheckChatsPinOrderArchivedCheck CheckConstraint = "chats_pin_order_archived_check" // chats
|
||||
CheckChatsPinOrderParentCheck CheckConstraint = "chats_pin_order_parent_check" // chats
|
||||
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
|
||||
|
||||
@@ -2682,6 +2682,17 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
|
||||
return q.db.GetAuthorizationUserRoles(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatACLByID(ctx context.Context, id uuid.UUID) (database.GetChatACLByIDRow, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, id)
|
||||
if err != nil {
|
||||
return database.GetChatACLByIDRow{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, chat); err != nil {
|
||||
return database.GetChatACLByIDRow{}, err
|
||||
}
|
||||
return q.db.GetChatACLByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAdvisorConfig(ctx context.Context) (string, error) {
|
||||
// The advisor configuration is a deployment-wide setting read by any
|
||||
// authenticated chat user and by chatd when deciding whether to attach
|
||||
@@ -2884,14 +2895,30 @@ func (q *querier) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.C
|
||||
if err != nil {
|
||||
return database.ChatFile{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, file); err != nil {
|
||||
fileAuthErr := q.authorizeContext(ctx, policy.ActionRead, file)
|
||||
if fileAuthErr == nil {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
prepared, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceChat.Type)
|
||||
if err != nil {
|
||||
return database.ChatFile{}, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
|
||||
}
|
||||
chats, err := q.db.GetAuthorizedChatsByChatFileID(ctx, id, prepared)
|
||||
if err != nil {
|
||||
return database.ChatFile{}, err
|
||||
}
|
||||
if len(chats) == 0 {
|
||||
return database.ChatFile{}, fileAuthErr
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetChatFileMetadataByChatID(ctx context.Context, chatID uuid.UUID) ([]database.GetChatFileMetadataByChatIDRow, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatFileMetadataByChatID)(ctx, chatID)
|
||||
if _, err := q.GetChatByID(ctx, chatID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatFileMetadataByChatID(ctx, chatID)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ChatFile, error) {
|
||||
@@ -2899,10 +2926,25 @@ func (q *querier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]dat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var prepared rbac.PreparedAuthorized
|
||||
for _, f := range files {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, f); err != nil {
|
||||
fileAuthErr := q.authorizeContext(ctx, policy.ActionRead, f)
|
||||
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, f.ID, prepared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(chats) == 0 {
|
||||
return nil, fileAuthErr
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
@@ -3164,6 +3206,10 @@ func (q *querier) GetChats(ctx context.Context, arg database.GetChatsParams) ([]
|
||||
return q.db.GetAuthorizedChats(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]database.Chat, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByChatFileID)(ctx, fileID)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByWorkspaceIDs)(ctx, ids)
|
||||
}
|
||||
@@ -6392,6 +6438,24 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatACLByID(ctx context.Context, arg database.UpdateChatACLByIDParams) error {
|
||||
if rbac.ChatACLDisabled() {
|
||||
return NotAuthorizedError{Err: xerrors.New("chat sharing is disabled")}
|
||||
}
|
||||
fetch := func(ctx context.Context, arg database.UpdateChatACLByIDParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
if chat.IsSubChat() {
|
||||
return database.Chat{}, NotAuthorizedError{Err: xerrors.New("chat ACLs can only be updated on root chats")}
|
||||
}
|
||||
return chat, nil
|
||||
}
|
||||
|
||||
return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.UpdateChatACLByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
@@ -8323,3 +8387,7 @@ func (q *querier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg
|
||||
func (q *querier) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, _ rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
|
||||
return q.GetChats(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
return q.db.GetAuthorizedChatsByChatFileID(ctx, fileID, prepared)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,108 @@ func TestNew(t *testing.T) {
|
||||
require.NoError(t, rec.AllAsserted(), "should only be 1 rbac call")
|
||||
}
|
||||
|
||||
func TestChatFilesAllowLinkedChatReads(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := dbauthz.As(context.Background(), rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Scope: rbac.ScopeAll,
|
||||
})
|
||||
authorizer := &coderdtest.FakeAuthorizer{
|
||||
ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error {
|
||||
if action == policy.ActionRead && object.Type == rbac.ResourceChat.Type {
|
||||
return xerrors.New("direct file auth denied")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("GetChatFileByID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
db := dbmock.NewMockStore(ctrl)
|
||||
file := testutil.Fake(t, gofakeit.New(0), database.ChatFile{})
|
||||
|
||||
db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
db.EXPECT().GetChatFileByID(gomock.Any(), file.ID).Return(file, nil)
|
||||
db.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), file.ID, gomock.Any()).Return([]database.Chat{{ID: uuid.New()}}, nil)
|
||||
|
||||
q := dbauthz.New(db, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
got, err := q.GetChatFileByID(ctx, file.ID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, file, got)
|
||||
})
|
||||
|
||||
t.Run("GetChatFilesByIDs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
db := dbmock.NewMockStore(ctrl)
|
||||
file := testutil.Fake(t, gofakeit.New(0), database.ChatFile{})
|
||||
|
||||
db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
db.EXPECT().GetChatFilesByIDs(gomock.Any(), []uuid.UUID{file.ID}).Return([]database.ChatFile{file}, nil)
|
||||
db.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), file.ID, gomock.Any()).Return([]database.Chat{{ID: uuid.New()}}, nil)
|
||||
|
||||
q := dbauthz.New(db, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
got, err := q.GetChatFilesByIDs(ctx, []uuid.UUID{file.ID})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []database.ChatFile{file}, got)
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // It toggles the global chat ACL flag.
|
||||
func TestUpdateChatACLByIDGuards(t *testing.T) {
|
||||
ctx := dbauthz.As(context.Background(), rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Scope: rbac.ScopeAll,
|
||||
})
|
||||
arg := database.UpdateChatACLByIDParams{
|
||||
ID: uuid.New(),
|
||||
UserACL: database.ChatACL{},
|
||||
GroupACL: database.ChatACL{},
|
||||
}
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) { //nolint:paralleltest // It toggles the global chat ACL flag.
|
||||
rbac.SetChatACLDisabled(true)
|
||||
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
db := dbmock.NewMockStore(ctrl)
|
||||
db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
|
||||
q := dbauthz.New(db, &coderdtest.FakeAuthorizer{}, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
err := q.UpdateChatACLByID(ctx, arg)
|
||||
|
||||
require.Error(t, err)
|
||||
require.True(t, dbauthz.IsNotAuthorizedError(err))
|
||||
require.ErrorContains(t, err, "chat sharing is disabled")
|
||||
})
|
||||
|
||||
t.Run("SubChat", func(t *testing.T) { //nolint:paralleltest // It depends on the global chat ACL flag.
|
||||
rbac.SetChatACLDisabled(false)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
db := dbmock.NewMockStore(ctrl)
|
||||
db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
db.EXPECT().GetChatByID(gomock.Any(), arg.ID).Return(database.Chat{
|
||||
ID: arg.ID,
|
||||
RootChatID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
ParentChatID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
}, nil)
|
||||
|
||||
q := dbauthz.New(db, &coderdtest.FakeAuthorizer{}, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
err := q.UpdateChatACLByID(ctx, arg)
|
||||
|
||||
require.Error(t, err)
|
||||
require.True(t, dbauthz.IsNotAuthorizedError(err))
|
||||
require.ErrorContains(t, err, "root chats")
|
||||
})
|
||||
}
|
||||
|
||||
// TestDBAuthzRecursive is a simple test to search for infinite recursion
|
||||
// bugs. It isn't perfect, and only catches a subset of the possible bugs
|
||||
// as only the first db call will be made. But it is better than nothing.
|
||||
@@ -585,6 +687,20 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().UpsertChatPersonalModelOverridesEnabled(gomock.Any(), true).Return(nil).AnyTimes()
|
||||
check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("GetChatACLByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
row := database.GetChatACLByIDRow{
|
||||
Users: database.ChatACL{
|
||||
uuid.NewString(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
Groups: database.ChatACL{
|
||||
uuid.NewString(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatACLByID(gomock.Any(), chat.ID).Return(row, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(row)
|
||||
}))
|
||||
s.Run("GetChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
@@ -722,14 +838,17 @@ func (s *MethodTestSuite) TestChats() {
|
||||
s.Run("GetChatFileByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
file := testutil.Fake(s.T(), faker, database.ChatFile{})
|
||||
dbm.EXPECT().GetChatFileByID(gomock.Any(), file.ID).Return(file, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), file.ID, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
|
||||
check.Args(file.ID).Asserts(rbac.ResourceChat.WithOwner(file.OwnerID.String()).InOrg(file.OrganizationID).WithID(file.ID), policy.ActionRead).Returns(file)
|
||||
}))
|
||||
s.Run("GetChatFilesByIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
file := testutil.Fake(s.T(), faker, database.ChatFile{})
|
||||
dbm.EXPECT().GetChatFilesByIDs(gomock.Any(), []uuid.UUID{file.ID}).Return([]database.ChatFile{file}, nil).AnyTimes()
|
||||
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("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{})
|
||||
rows := []database.GetChatFileMetadataByChatIDRow{{
|
||||
ID: file.ID,
|
||||
@@ -739,8 +858,9 @@ func (s *MethodTestSuite) TestChats() {
|
||||
OwnerID: file.OwnerID,
|
||||
OrganizationID: file.OrganizationID,
|
||||
}}
|
||||
dbm.EXPECT().GetChatFileMetadataByChatID(gomock.Any(), file.ID).Return(rows, nil).AnyTimes()
|
||||
check.Args(file.ID).Asserts(rbac.ResourceChat.WithOwner(file.OwnerID.String()).InOrg(file.OrganizationID).WithID(file.ID), policy.ActionRead).Returns(rows)
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatFileMetadataByChatID(gomock.Any(), chat.ID).Return(rows, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(rows)
|
||||
}))
|
||||
s.Run("DeleteOldChatDebugRuns", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().DeleteOldChatDebugRuns(gomock.Any(), database.DeleteOldChatDebugRunsParams{}).Return(int64(0), nil).AnyTimes()
|
||||
@@ -886,6 +1006,14 @@ func (s *MethodTestSuite) TestChats() {
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params).Asserts()
|
||||
}))
|
||||
s.Run("GetChatsByChatFileID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chatA := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
chatB := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
fileID := uuid.New()
|
||||
chats := []database.Chat{chatA, chatB}
|
||||
dbm.EXPECT().GetChatsByChatFileID(gomock.Any(), fileID).Return(chats, nil).AnyTimes()
|
||||
check.Args(fileID).Asserts(chatA, policy.ActionRead, chatB, policy.ActionRead).Returns(chats)
|
||||
}))
|
||||
s.Run("GetChildChatsByParentIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
parentA := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
parentB := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
@@ -913,6 +1041,12 @@ func (s *MethodTestSuite) TestChats() {
|
||||
// No asserts here because it re-routes through GetChats which uses SQLFilter.
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
s.Run("GetAuthorizedChatsByChatFileID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
fileID := uuid.New()
|
||||
dbm.EXPECT().GetAuthorizedChatsByChatFileID(gomock.Any(), fileID, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
|
||||
// No asserts here because callers provide the SQL filter.
|
||||
check.Args(fileID, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
s.Run("GetChatQueuedMessages", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
qms := []database.ChatQueuedMessage{testutil.Fake(s.T(), faker, database.ChatQueuedMessage{})}
|
||||
@@ -1057,6 +1191,19 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().ReorderChatQueuedMessageToFront(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
|
||||
}))
|
||||
s.Run("UpdateChatACLByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
chat.RootChatID = uuid.NullUUID{}
|
||||
chat.ParentChatID = uuid.NullUUID{}
|
||||
arg := database.UpdateChatACLByIDParams{
|
||||
ID: chat.ID,
|
||||
UserACL: database.ChatACL{},
|
||||
GroupACL: database.ChatACL{},
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatACLByID(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionShare).Returns()
|
||||
}))
|
||||
s.Run("UpdateChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatByIDParams{
|
||||
|
||||
@@ -1209,6 +1209,14 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatACLByID(ctx context.Context, id uuid.UUID) (database.GetChatACLByIDRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatACLByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetChatACLByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatACLByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAdvisorConfig(ctx context.Context) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAdvisorConfig(ctx)
|
||||
@@ -1625,6 +1633,14 @@ func (m queryMetricsStore) GetChats(ctx context.Context, arg database.GetChatsPa
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatsByChatFileID(ctx, fileID)
|
||||
m.queryLatencies.WithLabelValues("GetChatsByChatFileID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatsByChatFileID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatsByWorkspaceIDs(ctx, ids)
|
||||
@@ -4641,6 +4657,14 @@ func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.Up
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatACLByID(ctx context.Context, arg database.UpdateChatACLByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateChatACLByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatACLByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatACLByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatBuildAgentBinding(ctx, arg)
|
||||
@@ -6160,3 +6184,11 @@ func (m queryMetricsStore) GetAuthorizedChats(ctx context.Context, arg database.
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChats").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedChatsByChatFileID(ctx, fileID, prepared)
|
||||
m.queryLatencies.WithLabelValues("GetAuthorizedChatsByChatFileID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChatsByChatFileID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
@@ -2146,6 +2146,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedChats(ctx, arg, prepared any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedChats", reflect.TypeOf((*MockStore)(nil).GetAuthorizedChats), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedChatsByChatFileID mocks base method.
|
||||
func (m *MockStore) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthorizedChatsByChatFileID", ctx, fileID, prepared)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAuthorizedChatsByChatFileID indicates an expected call of GetAuthorizedChatsByChatFileID.
|
||||
func (mr *MockStoreMockRecorder) GetAuthorizedChatsByChatFileID(ctx, fileID, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedChatsByChatFileID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedChatsByChatFileID), ctx, fileID, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedConnectionLogsOffset mocks base method.
|
||||
func (m *MockStore) GetAuthorizedConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams, prepared rbac.PreparedAuthorized) ([]database.GetConnectionLogsOffsetRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2221,6 +2236,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx,
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared)
|
||||
}
|
||||
|
||||
// GetChatACLByID mocks base method.
|
||||
func (m *MockStore) GetChatACLByID(ctx context.Context, id uuid.UUID) (database.GetChatACLByIDRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatACLByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.GetChatACLByIDRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatACLByID indicates an expected call of GetChatACLByID.
|
||||
func (mr *MockStoreMockRecorder) GetChatACLByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatACLByID", reflect.TypeOf((*MockStore)(nil).GetChatACLByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetChatAdvisorConfig mocks base method.
|
||||
func (m *MockStore) GetChatAdvisorConfig(ctx context.Context) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3001,6 +3031,21 @@ func (mr *MockStoreMockRecorder) GetChats(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChats", reflect.TypeOf((*MockStore)(nil).GetChats), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatsByChatFileID mocks base method.
|
||||
func (m *MockStore) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatsByChatFileID", ctx, fileID)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatsByChatFileID indicates an expected call of GetChatsByChatFileID.
|
||||
func (mr *MockStoreMockRecorder) GetChatsByChatFileID(ctx, fileID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatsByChatFileID", reflect.TypeOf((*MockStore)(nil).GetChatsByChatFileID), ctx, fileID)
|
||||
}
|
||||
|
||||
// GetChatsByWorkspaceIDs mocks base method.
|
||||
func (m *MockStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8774,6 +8819,20 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatACLByID mocks base method.
|
||||
func (m *MockStore) UpdateChatACLByID(ctx context.Context, arg database.UpdateChatACLByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatACLByID", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateChatACLByID indicates an expected call of UpdateChatACLByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatACLByID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatACLByID", reflect.TypeOf((*MockStore)(nil).UpdateChatACLByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatBuildAgentBinding mocks base method.
|
||||
func (m *MockStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -429,6 +429,25 @@ func chatFromAutoArchiveRow(logger slog.Logger, r database.AutoArchiveInactiveCh
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
var userACL database.ChatACL
|
||||
if err := userACL.Scan([]byte(r.UserACL)); err != nil {
|
||||
logger.Warn(context.Background(), "failed to parse chat user ACL from auto-archive row",
|
||||
slog.F("chat_id", r.ID),
|
||||
slog.F("raw_user_acl", string(r.UserACL)),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
var groupACL database.ChatACL
|
||||
if err := groupACL.Scan([]byte(r.GroupACL)); err != nil {
|
||||
logger.Warn(context.Background(), "failed to parse chat group ACL from auto-archive row",
|
||||
slog.F("chat_id", r.ID),
|
||||
slog.F("raw_group_acl", string(r.GroupACL)),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
return database.Chat{
|
||||
ID: r.ID,
|
||||
OwnerID: r.OwnerID,
|
||||
@@ -451,6 +470,8 @@ func chatFromAutoArchiveRow(logger slog.Logger, r database.AutoArchiveInactiveCh
|
||||
Mode: r.Mode,
|
||||
MCPServerIDs: r.MCPServerIDs,
|
||||
Labels: labels,
|
||||
UserACL: userACL,
|
||||
GroupACL: groupACL,
|
||||
PinOrder: r.PinOrder,
|
||||
LastReadMessageID: r.LastReadMessageID,
|
||||
LastInjectedContext: r.LastInjectedContext,
|
||||
|
||||
Generated
+11
-2
@@ -242,7 +242,8 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'ai_provider:create',
|
||||
'ai_provider:delete',
|
||||
'ai_provider:read',
|
||||
'ai_provider:update'
|
||||
'ai_provider:update',
|
||||
'chat:share'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -1558,6 +1559,11 @@ CREATE TABLE chats (
|
||||
plan_mode chat_plan_mode,
|
||||
client_type chat_client_type DEFAULT 'api'::chat_client_type NOT NULL,
|
||||
last_turn_summary text,
|
||||
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
CONSTRAINT chat_acl_only_on_root_chats CHECK ((((parent_chat_id IS NULL) AND (root_chat_id IS NULL)) OR ((user_acl = '{}'::jsonb) AND (group_acl = '{}'::jsonb)))),
|
||||
CONSTRAINT chat_group_acl_not_null_jsonb CHECK (((group_acl IS NOT NULL) AND (jsonb_typeof(group_acl) = 'object'::text))),
|
||||
CONSTRAINT chat_user_acl_not_null_jsonb CHECK (((user_acl IS NOT NULL) AND (jsonb_typeof(user_acl) = 'object'::text))),
|
||||
CONSTRAINT chats_pin_order_archived_check CHECK (((pin_order = 0) OR (archived = false))),
|
||||
CONSTRAINT chats_pin_order_parent_check CHECK (((pin_order = 0) OR (parent_chat_id IS NULL)))
|
||||
);
|
||||
@@ -1642,9 +1648,12 @@ CREATE VIEW chats_expanded AS
|
||||
c.plan_mode,
|
||||
c.client_type,
|
||||
c.last_turn_summary,
|
||||
COALESCE(root.user_acl, c.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, c.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM (chats c
|
||||
FROM ((chats c
|
||||
LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id))))
|
||||
JOIN visible_users owner ON ((owner.id = c.owner_id)));
|
||||
|
||||
CREATE TABLE connection_logs (
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
DROP VIEW IF EXISTS chats_expanded;
|
||||
|
||||
ALTER TABLE chats DROP CONSTRAINT IF EXISTS chat_acl_only_on_root_chats;
|
||||
ALTER TABLE chats DROP CONSTRAINT IF EXISTS chat_group_acl_not_null_jsonb;
|
||||
ALTER TABLE chats DROP CONSTRAINT IF EXISTS chat_user_acl_not_null_jsonb;
|
||||
ALTER TABLE chats DROP COLUMN IF EXISTS group_acl;
|
||||
ALTER TABLE chats DROP COLUMN IF EXISTS user_acl;
|
||||
|
||||
CREATE VIEW chats_expanded AS
|
||||
SELECT
|
||||
c.id,
|
||||
c.owner_id,
|
||||
c.workspace_id,
|
||||
c.title,
|
||||
c.status,
|
||||
c.worker_id,
|
||||
c.started_at,
|
||||
c.heartbeat_at,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.parent_chat_id,
|
||||
c.root_chat_id,
|
||||
c.last_model_config_id,
|
||||
c.archived,
|
||||
c.last_error,
|
||||
c.mode,
|
||||
c.mcp_server_ids,
|
||||
c.labels,
|
||||
c.build_id,
|
||||
c.agent_id,
|
||||
c.pin_order,
|
||||
c.last_read_message_id,
|
||||
c.last_injected_context,
|
||||
c.dynamic_tools,
|
||||
c.organization_id,
|
||||
c.plan_mode,
|
||||
c.client_type,
|
||||
c.last_turn_summary,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
chats c
|
||||
JOIN visible_users owner ON owner.id = c.owner_id;
|
||||
|
||||
-- Intentionally leave chat:share in api_key_scope because PostgreSQL cannot remove enum values.
|
||||
@@ -0,0 +1,60 @@
|
||||
DROP VIEW IF EXISTS chats_expanded;
|
||||
|
||||
ALTER TABLE chats
|
||||
ADD COLUMN user_acl jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN group_acl jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
ALTER TABLE chats
|
||||
ADD CONSTRAINT chat_user_acl_not_null_jsonb
|
||||
CHECK (user_acl IS NOT NULL AND jsonb_typeof(user_acl) = 'object'),
|
||||
ADD CONSTRAINT chat_group_acl_not_null_jsonb
|
||||
CHECK (group_acl IS NOT NULL AND jsonb_typeof(group_acl) = 'object'),
|
||||
ADD CONSTRAINT chat_acl_only_on_root_chats
|
||||
CHECK (
|
||||
(parent_chat_id IS NULL AND root_chat_id IS NULL)
|
||||
OR (
|
||||
user_acl = '{}'::jsonb
|
||||
AND group_acl = '{}'::jsonb
|
||||
)
|
||||
);
|
||||
|
||||
CREATE VIEW chats_expanded AS
|
||||
SELECT
|
||||
c.id,
|
||||
c.owner_id,
|
||||
c.workspace_id,
|
||||
c.title,
|
||||
c.status,
|
||||
c.worker_id,
|
||||
c.started_at,
|
||||
c.heartbeat_at,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.parent_chat_id,
|
||||
c.root_chat_id,
|
||||
c.last_model_config_id,
|
||||
c.archived,
|
||||
c.last_error,
|
||||
c.mode,
|
||||
c.mcp_server_ids,
|
||||
c.labels,
|
||||
c.build_id,
|
||||
c.agent_id,
|
||||
c.pin_order,
|
||||
c.last_read_message_id,
|
||||
c.last_injected_context,
|
||||
c.dynamic_tools,
|
||||
c.organization_id,
|
||||
c.plan_mode,
|
||||
c.client_type,
|
||||
c.last_turn_summary,
|
||||
COALESCE(root.user_acl, c.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, c.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
chats c
|
||||
LEFT JOIN chats root ON root.id = COALESCE(c.root_chat_id, c.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = c.owner_id;
|
||||
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat:share';
|
||||
@@ -194,7 +194,22 @@ func (t Task) RBACObject() rbac.Object {
|
||||
}
|
||||
|
||||
func (c Chat) RBACObject() rbac.Object {
|
||||
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
|
||||
obj := rbac.ResourceChat.
|
||||
WithID(c.ID).
|
||||
WithOwner(c.OwnerID.String()).
|
||||
InOrg(c.OrganizationID)
|
||||
|
||||
if rbac.ChatACLDisabled() {
|
||||
return obj
|
||||
}
|
||||
|
||||
return obj.
|
||||
WithACLUserList(c.UserACL.RBACACL()).
|
||||
WithGroupACL(c.GroupACL.RBACACL())
|
||||
}
|
||||
|
||||
func (c Chat) IsSubChat() bool {
|
||||
return c.RootChatID.Valid || c.ParentChatID.Valid
|
||||
}
|
||||
|
||||
func (r GetChatsRow) RBACObject() rbac.Object {
|
||||
|
||||
@@ -143,6 +143,45 @@ func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestChatACLDisabled(t *testing.T) {
|
||||
uid := uuid.NewString()
|
||||
gid := uuid.NewString()
|
||||
|
||||
chat := Chat{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: uuid.New(),
|
||||
OwnerID: uuid.New(),
|
||||
UserACL: ChatACL{
|
||||
uid: ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
GroupACL: ChatACL{
|
||||
gid: ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("ACLsOmittedWhenDisabled", func(t *testing.T) {
|
||||
rbac.SetChatACLDisabled(true)
|
||||
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
||||
|
||||
obj := chat.RBACObject()
|
||||
|
||||
require.Empty(t, obj.ACLUserList, "user ACLs should be empty when disabled")
|
||||
require.Empty(t, obj.ACLGroupList, "group ACLs should be empty when disabled")
|
||||
})
|
||||
|
||||
t.Run("ACLsIncludedWhenEnabled", func(t *testing.T) {
|
||||
rbac.SetChatACLDisabled(false)
|
||||
|
||||
obj := chat.RBACObject()
|
||||
|
||||
require.NotEmpty(t, obj.ACLUserList, "user ACLs should be present when enabled")
|
||||
require.NotEmpty(t, obj.ACLGroupList, "group ACLs should be present when enabled")
|
||||
require.Contains(t, obj.ACLUserList, uid)
|
||||
require.Contains(t, obj.ACLGroupList, gid)
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestWorkspaceACLDisabled(t *testing.T) {
|
||||
uid := uuid.NewString()
|
||||
|
||||
@@ -744,9 +744,17 @@ func (q *sqlQuerier) CountAuthorizedConnectionLogs(ctx context.Context, arg Coun
|
||||
|
||||
type chatQuerier interface {
|
||||
GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]GetChatsRow, error)
|
||||
GetAuthorizedChatsByChatFileID(ctx context.Context, fileID uuid.UUID, prepared rbac.PreparedAuthorized) ([]Chat, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]GetChatsRow, error) {
|
||||
if arg.OwnedOnly && arg.SharedOnly {
|
||||
return nil, xerrors.New("owned_only and shared_only cannot both be true")
|
||||
}
|
||||
if (arg.OwnedOnly || arg.SharedOnly) && arg.ViewerID == uuid.Nil {
|
||||
return nil, xerrors.New("viewer_id required when owned_only or shared_only is true")
|
||||
}
|
||||
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigChats())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
@@ -760,7 +768,9 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedChats :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query,
|
||||
arg.OwnerID,
|
||||
arg.OwnedOnly,
|
||||
arg.ViewerID,
|
||||
arg.SharedOnly,
|
||||
arg.Archived,
|
||||
arg.AfterID,
|
||||
arg.LabelFilter,
|
||||
@@ -804,6 +814,8 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
&i.Chat.PlanMode,
|
||||
&i.Chat.ClientType,
|
||||
&i.Chat.LastTurnSummary,
|
||||
&i.Chat.UserACL,
|
||||
&i.Chat.GroupACL,
|
||||
&i.Chat.OwnerUsername,
|
||||
&i.Chat.OwnerName,
|
||||
&i.HasUnread); err != nil {
|
||||
@@ -820,6 +832,72 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID uuid.UUID, prepared rbac.PreparedAuthorized) ([]Chat, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigChats())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
}
|
||||
|
||||
filtered, err := insertAuthorizedFilter(getChatsByChatFileID, fmt.Sprintf(" AND %s\nLIMIT 1", authorizedFilter))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert authorized filter: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedChatsByChatFileID :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query, fileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Chat
|
||||
for rows.Next() {
|
||||
var i Chat
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.OwnerID,
|
||||
&i.WorkspaceID,
|
||||
&i.Title,
|
||||
&i.Status,
|
||||
&i.WorkerID,
|
||||
&i.StartedAt,
|
||||
&i.HeartbeatAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentChatID,
|
||||
&i.RootChatID,
|
||||
&i.LastModelConfigID,
|
||||
&i.Archived,
|
||||
&i.LastError,
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.BuildID,
|
||||
&i.AgentID,
|
||||
&i.PinOrder,
|
||||
&i.LastReadMessageID,
|
||||
&i.LastInjectedContext,
|
||||
&i.DynamicTools,
|
||||
&i.OrganizationID,
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName); 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
|
||||
}
|
||||
|
||||
type aibridgeQuerier interface {
|
||||
ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error)
|
||||
CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) (int64, error)
|
||||
|
||||
@@ -311,6 +311,7 @@ const (
|
||||
ApiKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
|
||||
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
||||
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
||||
ApiKeyScopeChatShare APIKeyScope = "chat:share"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -565,7 +566,8 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeAiProviderCreate,
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate:
|
||||
ApiKeyScopeAiProviderUpdate,
|
||||
ApiKeyScopeChatShare:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -789,6 +791,7 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate,
|
||||
ApiKeyScopeChatShare,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4533,6 +4536,8 @@ type Chat struct {
|
||||
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
|
||||
ClientType ChatClientType `db:"client_type" json:"client_type"`
|
||||
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
|
||||
UserACL ChatACL `db:"user_acl" json:"user_acl"`
|
||||
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
OwnerName string `db:"owner_name" json:"owner_name"`
|
||||
}
|
||||
@@ -4712,6 +4717,8 @@ type ChatTable struct {
|
||||
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
|
||||
ClientType ChatClientType `db:"client_type" json:"client_type"`
|
||||
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
|
||||
UserACL ChatACL `db:"user_acl" json:"user_acl"`
|
||||
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
|
||||
}
|
||||
|
||||
type ChatUsageLimitConfig struct {
|
||||
|
||||
@@ -298,6 +298,7 @@ type sqlcQuerier interface {
|
||||
// This function returns roles for authorization purposes. Implied member roles
|
||||
// are included.
|
||||
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
|
||||
GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error)
|
||||
// GetChatAdvisorConfig returns the deployment-wide runtime configuration
|
||||
// for the experimental chat advisor as a JSON blob. Callers unmarshal the
|
||||
// result into codersdk.AdvisorConfig. Returns '{}' when unset so zero
|
||||
@@ -411,6 +412,7 @@ type sqlcQuerier interface {
|
||||
// Returns "0s" (disabled) when no value has been configured.
|
||||
GetChatWorkspaceTTL(ctx context.Context) (string, error)
|
||||
GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error)
|
||||
GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]Chat, error)
|
||||
GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]Chat, error)
|
||||
// Retrieves chats updated after the given timestamp for telemetry
|
||||
// snapshot collection. Uses updated_at so that long-running chats
|
||||
@@ -1132,6 +1134,7 @@ type sqlcQuerier interface {
|
||||
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
|
||||
UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateChatACLByID(ctx context.Context, arg UpdateChatACLByIDParams) error
|
||||
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
|
||||
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
|
||||
// Uses COALESCE so that passing NULL from Go means "keep the
|
||||
|
||||
+316
-10
@@ -1362,19 +1362,31 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(sameOrgAdminRows), 5, "same-org admin should see all chats in their org")
|
||||
|
||||
// OwnerID filter: member queries their own chats.
|
||||
// OwnedOnly filter: member queries their own chats.
|
||||
memberFilterSelf, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
OwnerID: member.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: member.ID,
|
||||
}, preparedMember)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberFilterSelf, 2)
|
||||
|
||||
// OwnerID filter: member queries owner's chats → sees 0.
|
||||
// OwnedOnly filter: member queries owner's chats and sees 0.
|
||||
memberFilterOwner, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
OwnerID: owner.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: owner.ID,
|
||||
}, preparedMember)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberFilterOwner, 0)
|
||||
|
||||
_, err = db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
OwnedOnly: true,
|
||||
}, preparedMember)
|
||||
require.ErrorContains(t, err, "viewer_id required")
|
||||
|
||||
_, err = db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
SharedOnly: true,
|
||||
}, preparedMember)
|
||||
require.ErrorContains(t, err, "viewer_id required")
|
||||
})
|
||||
|
||||
t.Run("dbauthz", func(t *testing.T) {
|
||||
@@ -1470,6 +1482,293 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // It toggles the global chat ACL flag.
|
||||
func TestGetAuthorizedChatsACLSharing(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
rbac.SetChatACLDisabled(false)
|
||||
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
owner := dbgen.User(t, db, database.User{})
|
||||
recipient := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: owner.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: recipient.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
|
||||
dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"})
|
||||
modelCfg := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Provider: "openai",
|
||||
Model: "test-model",
|
||||
CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
IsDefault: true,
|
||||
CompressionThreshold: 80,
|
||||
})
|
||||
|
||||
ownerChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: owner.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "shared owner chat",
|
||||
})
|
||||
recipientChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: recipient.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "recipient chat",
|
||||
})
|
||||
|
||||
sharedACL := database.ChatACL{
|
||||
recipient.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
}
|
||||
err = db.UpdateChatACLByID(ctx, database.UpdateChatACLByIDParams{
|
||||
ID: ownerChat.ID,
|
||||
UserACL: sharedACL,
|
||||
GroupACL: database.ChatACL{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientSubject, _, err := httpmw.UserRBACSubject(ctx, db, recipient.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
preparedRecipient, err := authorizer.Prepare(ctx, recipientSubject, policy.ActionRead, rbac.ResourceChat.Type)
|
||||
require.NoError(t, err)
|
||||
|
||||
chatIDs := func(rows []database.GetChatsRow) []uuid.UUID {
|
||||
ids := make([]uuid.UUID, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, row.Chat.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
rows, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{}, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []uuid.UUID{ownerChat.ID, recipientChat.ID}, chatIDs(rows))
|
||||
|
||||
sharedOnly, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
SharedOnly: true,
|
||||
ViewerID: recipient.ID,
|
||||
}, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []uuid.UUID{ownerChat.ID}, chatIDs(sharedOnly))
|
||||
require.Equal(t, sharedACL, sharedOnly[0].Chat.UserACL)
|
||||
require.Empty(t, sharedOnly[0].Chat.GroupACL)
|
||||
|
||||
_, err = db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
OwnedOnly: true,
|
||||
SharedOnly: true,
|
||||
ViewerID: recipient.ID,
|
||||
}, preparedRecipient)
|
||||
require.ErrorContains(t, err, "owned_only and shared_only")
|
||||
|
||||
authzdb := dbauthz.New(db, authorizer, slogtest.Make(t, &slogtest.Options{}), coderdtest.AccessControlStorePointer())
|
||||
recipientCtx := dbauthz.As(ctx, recipientSubject)
|
||||
authzRows, err := authzdb.GetChats(recipientCtx, database.GetChatsParams{})
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []uuid.UUID{ownerChat.ID, recipientChat.ID}, chatIDs(authzRows))
|
||||
|
||||
rbac.SetChatACLDisabled(true)
|
||||
disabledRows, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{}, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []uuid.UUID{recipientChat.ID}, chatIDs(disabledRows))
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // It toggles the global chat ACL flag.
|
||||
func TestGetAuthorizedChatsACLSharingGroupACL(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
rbac.SetChatACLDisabled(false)
|
||||
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
owner := dbgen.User(t, db, database.User{})
|
||||
recipient := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: owner.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: recipient.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: recipient.ID, GroupID: group.ID})
|
||||
|
||||
dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"})
|
||||
modelCfg := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Provider: "openai",
|
||||
Model: "test-model",
|
||||
CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
IsDefault: true,
|
||||
CompressionThreshold: 80,
|
||||
})
|
||||
|
||||
ownerChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: owner.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "shared owner chat",
|
||||
})
|
||||
recipientChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: recipient.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "recipient chat",
|
||||
})
|
||||
|
||||
sharedGroupACL := database.ChatACL{
|
||||
group.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
}
|
||||
err = db.UpdateChatACLByID(ctx, database.UpdateChatACLByIDParams{
|
||||
ID: ownerChat.ID,
|
||||
UserACL: database.ChatACL{},
|
||||
GroupACL: sharedGroupACL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientSubject, _, err := httpmw.UserRBACSubject(ctx, db, recipient.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
preparedRecipient, err := authorizer.Prepare(ctx, recipientSubject, policy.ActionRead, rbac.ResourceChat.Type)
|
||||
require.NoError(t, err)
|
||||
|
||||
chatIDs := func(rows []database.GetChatsRow) []uuid.UUID {
|
||||
ids := make([]uuid.UUID, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, row.Chat.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
rows, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{}, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []uuid.UUID{ownerChat.ID, recipientChat.ID}, chatIDs(rows))
|
||||
|
||||
sharedOnly, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{
|
||||
SharedOnly: true,
|
||||
ViewerID: recipient.ID,
|
||||
}, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sharedOnly, 1)
|
||||
require.Equal(t, ownerChat.ID, sharedOnly[0].Chat.ID)
|
||||
require.Empty(t, sharedOnly[0].Chat.UserACL)
|
||||
require.Equal(t, sharedGroupACL, sharedOnly[0].Chat.GroupACL)
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // It toggles the global chat ACL flag.
|
||||
func TestGetAuthorizedChatsByChatFileIDACLSharing(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
rbac.SetChatACLDisabled(false)
|
||||
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
owner := dbgen.User(t, db, database.User{})
|
||||
recipient := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: owner.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
UserID: recipient.ID,
|
||||
OrganizationID: org.ID,
|
||||
Roles: []string{rbac.RoleAgentsAccess()},
|
||||
})
|
||||
|
||||
dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"})
|
||||
modelCfg := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Provider: "openai",
|
||||
Model: "test-model",
|
||||
CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
IsDefault: true,
|
||||
CompressionThreshold: 80,
|
||||
})
|
||||
|
||||
ownerChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: owner.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "shared owner chat",
|
||||
})
|
||||
sharedACL := database.ChatACL{
|
||||
recipient.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
}
|
||||
err = db.UpdateChatACLByID(ctx, database.UpdateChatACLByIDParams{
|
||||
ID: ownerChat.ID,
|
||||
UserACL: sharedACL,
|
||||
GroupACL: database.ChatACL{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
fileRow, err := db.InsertChatFile(ctx, database.InsertChatFileParams{
|
||||
OwnerID: owner.ID,
|
||||
OrganizationID: org.ID,
|
||||
Name: "shared.txt",
|
||||
Mimetype: "text/plain",
|
||||
Data: []byte("shared file"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rejected, err := db.LinkChatFiles(ctx, database.LinkChatFilesParams{
|
||||
ChatID: ownerChat.ID,
|
||||
FileIds: []uuid.UUID{fileRow.ID},
|
||||
MaxFileLinks: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, rejected)
|
||||
|
||||
recipientSubject, _, err := httpmw.UserRBACSubject(ctx, db, recipient.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
preparedRecipient, err := authorizer.Prepare(ctx, recipientSubject, policy.ActionRead, rbac.ResourceChat.Type)
|
||||
require.NoError(t, err)
|
||||
|
||||
rows, err := db.GetAuthorizedChatsByChatFileID(ctx, fileRow.ID, preparedRecipient)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1)
|
||||
require.Equal(t, ownerChat.ID, rows[0].ID)
|
||||
require.Equal(t, sharedACL, rows[0].UserACL)
|
||||
require.Empty(t, rows[0].GroupACL)
|
||||
}
|
||||
|
||||
func TestInsertWorkspaceAgentLogs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
@@ -11783,7 +12082,10 @@ func TestChatLabels(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rows, err := db.GetChats(ctx, database.GetChatsParams{OwnerID: owner.ID})
|
||||
rows, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnedOnly: true,
|
||||
ViewerID: owner.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
chatIndex := slices.IndexFunc(rows, func(row database.GetChatsRow) bool {
|
||||
@@ -11935,7 +12237,8 @@ func TestChatLabels(t *testing.T) {
|
||||
filterJSON, err := json.Marshal(database.StringMap{"env": "prod"})
|
||||
require.NoError(t, err)
|
||||
results, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: owner.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: owner.ID,
|
||||
LabelFilter: pqtype.NullRawMessage{
|
||||
RawMessage: filterJSON,
|
||||
Valid: true,
|
||||
@@ -11955,7 +12258,8 @@ func TestChatLabels(t *testing.T) {
|
||||
filterJSON, err = json.Marshal(database.StringMap{"env": "prod", "team": "backend"})
|
||||
require.NoError(t, err)
|
||||
results, err = db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: owner.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: owner.ID,
|
||||
LabelFilter: pqtype.NullRawMessage{
|
||||
RawMessage: filterJSON,
|
||||
Valid: true,
|
||||
@@ -11964,9 +12268,10 @@ func TestChatLabels(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
require.Equal(t, "filter-a", results[0].Chat.Title)
|
||||
// No filter — should return all chats for this owner.
|
||||
// No filter should return all chats for this owner.
|
||||
allChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: owner.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: owner.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(allChats), 3)
|
||||
@@ -13688,7 +13993,8 @@ func TestChatHasUnread(t *testing.T) {
|
||||
|
||||
getHasUnread := func() bool {
|
||||
rows, err := store.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
for _, row := range rows {
|
||||
|
||||
+272
-53
@@ -5728,7 +5728,7 @@ WHERE
|
||||
LIMIT
|
||||
$3::int
|
||||
)
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -5760,13 +5760,16 @@ chats_expanded AS (
|
||||
acquired_chats.plan_mode,
|
||||
acquired_chats.client_type,
|
||||
acquired_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
acquired_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = acquired_chats.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -5816,6 +5819,8 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) (
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -5956,7 +5961,7 @@ WITH updated_chats AS (
|
||||
UPDATE chats
|
||||
SET archived = true, pin_order = 0, updated_at = NOW()
|
||||
WHERE id = $1::uuid OR root_chat_id = $1::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -5988,13 +5993,16 @@ chats_expanded AS (
|
||||
updated_chats.plan_mode,
|
||||
updated_chats.client_type,
|
||||
updated_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chats.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
|
||||
`
|
||||
@@ -6037,6 +6045,8 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat,
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -6088,10 +6098,10 @@ archived AS (
|
||||
FROM to_archive t
|
||||
WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children
|
||||
AND c.archived = false
|
||||
RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary
|
||||
RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl
|
||||
)
|
||||
SELECT
|
||||
a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary,
|
||||
a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl,
|
||||
-- Children inherit their root's activity so last_activity_at is never null.
|
||||
COALESCE(
|
||||
t.last_activity_at,
|
||||
@@ -6137,6 +6147,8 @@ type AutoArchiveInactiveChatsRow struct {
|
||||
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
|
||||
ClientType ChatClientType `db:"client_type" json:"client_type"`
|
||||
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
|
||||
UserACL json.RawMessage `db:"user_acl" json:"user_acl"`
|
||||
GroupACL json.RawMessage `db:"group_acl" json:"group_acl"`
|
||||
LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"`
|
||||
}
|
||||
|
||||
@@ -6183,6 +6195,8 @@ func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchi
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.LastActivityAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -6333,7 +6347,7 @@ func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParam
|
||||
}
|
||||
|
||||
const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
WHERE agent_id = $1::uuid
|
||||
AND archived = false
|
||||
@@ -6382,6 +6396,8 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -6398,8 +6414,30 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getChatACLByID = `-- name: GetChatACLByID :one
|
||||
SELECT
|
||||
user_acl AS users,
|
||||
group_acl AS groups
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
`
|
||||
|
||||
type GetChatACLByIDRow struct {
|
||||
Users ChatACL `db:"users" json:"users"`
|
||||
Groups ChatACL `db:"groups" json:"groups"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getChatACLByID, id)
|
||||
var i GetChatACLByIDRow
|
||||
err := row.Scan(&i.Users, &i.Groups)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatByID = `-- name: GetChatByID :one
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
WHERE id = $1::uuid
|
||||
`
|
||||
@@ -6436,6 +6474,8 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -6444,7 +6484,7 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
|
||||
|
||||
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
|
||||
WITH locked_chat AS (
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
FROM chats
|
||||
WHERE id = $1::uuid
|
||||
FOR UPDATE
|
||||
@@ -6479,12 +6519,16 @@ chats_expanded AS (
|
||||
locked_chat.plan_mode,
|
||||
locked_chat.client_type,
|
||||
locked_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM locked_chat
|
||||
FROM
|
||||
locked_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = locked_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -6520,6 +6564,8 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -7700,10 +7746,10 @@ WITH cursor_chat AS (
|
||||
updated_at,
|
||||
id
|
||||
FROM chats
|
||||
WHERE id = $3
|
||||
WHERE id = $5
|
||||
)
|
||||
SELECT
|
||||
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.owner_username, chats_expanded.owner_name,
|
||||
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name,
|
||||
EXISTS (
|
||||
SELECT 1 FROM chat_messages cm
|
||||
WHERE cm.chat_id = chats_expanded.id
|
||||
@@ -7715,19 +7761,23 @@ FROM
|
||||
chats_expanded
|
||||
WHERE
|
||||
CASE
|
||||
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chats_expanded.owner_id = $1
|
||||
WHEN $1::boolean THEN chats_expanded.owner_id = $2::uuid
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $2 :: boolean IS NULL THEN true
|
||||
ELSE chats_expanded.archived = $2 :: boolean
|
||||
WHEN $3::boolean THEN chats_expanded.owner_id != $2::uuid
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $4 :: boolean IS NULL THEN true
|
||||
ELSE chats_expanded.archived = $4 :: boolean
|
||||
END
|
||||
AND CASE
|
||||
-- Cursor pagination: the last element on a page acts as the cursor.
|
||||
-- The 4-tuple matches the ORDER BY below. All columns sort DESC
|
||||
-- (pin_order is negated so lower values sort first in DESC order),
|
||||
-- which lets us use a single tuple < comparison.
|
||||
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
(CASE WHEN chats_expanded.pin_order > 0 THEN 1 ELSE 0 END, -chats_expanded.pin_order, chats_expanded.updated_at, chats_expanded.id) < (
|
||||
SELECT
|
||||
CASE WHEN cursor_chat.pin_order > 0 THEN 1 ELSE 0 END,
|
||||
@@ -7741,7 +7791,7 @@ WHERE
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $4::jsonb IS NOT NULL THEN chats_expanded.labels @> $4::jsonb
|
||||
WHEN $6::jsonb IS NOT NULL THEN chats_expanded.labels @> $6::jsonb
|
||||
ELSE true
|
||||
END
|
||||
-- Match chats whose linked diff URL (e.g. a pull request URL)
|
||||
@@ -7749,13 +7799,13 @@ WHERE
|
||||
-- a delegated sub-agent's diff status, so we surface the root chat
|
||||
-- when any descendant matches.
|
||||
AND CASE
|
||||
WHEN $5::text IS NOT NULL THEN EXISTS (
|
||||
WHEN $7::text IS NOT NULL THEN EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_diff_statuses cds
|
||||
JOIN chats c2 ON c2.id = cds.chat_id
|
||||
WHERE cds.url IS NOT NULL
|
||||
AND cds.url <> ''
|
||||
AND LOWER(cds.url) = LOWER($5::text)
|
||||
AND LOWER(cds.url) = LOWER($7::text)
|
||||
AND (c2.id = chats_expanded.id OR c2.root_chat_id = chats_expanded.id)
|
||||
)
|
||||
ELSE true
|
||||
@@ -7776,15 +7826,17 @@ ORDER BY
|
||||
-chats_expanded.pin_order DESC,
|
||||
chats_expanded.updated_at DESC,
|
||||
chats_expanded.id DESC
|
||||
OFFSET $6
|
||||
OFFSET $8
|
||||
LIMIT
|
||||
-- The chat list is unbounded and expected to grow large.
|
||||
-- Default to 50 to prevent accidental excessively large queries.
|
||||
COALESCE(NULLIF($7 :: int, 0), 50)
|
||||
COALESCE(NULLIF($9 :: int, 0), 50)
|
||||
`
|
||||
|
||||
type GetChatsParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OwnedOnly bool `db:"owned_only" json:"owned_only"`
|
||||
ViewerID uuid.UUID `db:"viewer_id" json:"viewer_id"`
|
||||
SharedOnly bool `db:"shared_only" json:"shared_only"`
|
||||
Archived sql.NullBool `db:"archived" json:"archived"`
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
LabelFilter pqtype.NullRawMessage `db:"label_filter" json:"label_filter"`
|
||||
@@ -7800,7 +7852,9 @@ type GetChatsRow struct {
|
||||
|
||||
func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChats,
|
||||
arg.OwnerID,
|
||||
arg.OwnedOnly,
|
||||
arg.ViewerID,
|
||||
arg.SharedOnly,
|
||||
arg.Archived,
|
||||
arg.AfterID,
|
||||
arg.LabelFilter,
|
||||
@@ -7844,6 +7898,8 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
|
||||
&i.Chat.PlanMode,
|
||||
&i.Chat.ClientType,
|
||||
&i.Chat.LastTurnSummary,
|
||||
&i.Chat.UserACL,
|
||||
&i.Chat.GroupACL,
|
||||
&i.Chat.OwnerUsername,
|
||||
&i.Chat.OwnerName,
|
||||
&i.HasUnread,
|
||||
@@ -7861,8 +7917,79 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many
|
||||
SELECT
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM
|
||||
chats_expanded
|
||||
WHERE
|
||||
id IN (
|
||||
SELECT chat_id
|
||||
FROM chat_file_links
|
||||
WHERE file_id = $1::uuid
|
||||
)
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedChatsByChatFileID.
|
||||
-- @authorize_filter
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]Chat, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChatsByChatFileID, fileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Chat
|
||||
for rows.Next() {
|
||||
var i Chat
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.OwnerID,
|
||||
&i.WorkspaceID,
|
||||
&i.Title,
|
||||
&i.Status,
|
||||
&i.WorkerID,
|
||||
&i.StartedAt,
|
||||
&i.HeartbeatAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentChatID,
|
||||
&i.RootChatID,
|
||||
&i.LastModelConfigID,
|
||||
&i.Archived,
|
||||
&i.LastError,
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.BuildID,
|
||||
&i.AgentID,
|
||||
&i.PinOrder,
|
||||
&i.LastReadMessageID,
|
||||
&i.LastInjectedContext,
|
||||
&i.DynamicTools,
|
||||
&i.OrganizationID,
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); 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 getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
WHERE archived = false
|
||||
AND workspace_id = ANY($1::uuid[])
|
||||
@@ -7907,6 +8034,8 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -7993,7 +8122,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time
|
||||
|
||||
const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many
|
||||
SELECT
|
||||
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.owner_username, chats_expanded.owner_name,
|
||||
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name,
|
||||
EXISTS (
|
||||
SELECT 1 FROM chat_messages cm
|
||||
WHERE cm.chat_id = chats_expanded.id
|
||||
@@ -8066,6 +8195,8 @@ func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildC
|
||||
&i.Chat.PlanMode,
|
||||
&i.Chat.ClientType,
|
||||
&i.Chat.LastTurnSummary,
|
||||
&i.Chat.UserACL,
|
||||
&i.Chat.GroupACL,
|
||||
&i.Chat.OwnerUsername,
|
||||
&i.Chat.OwnerName,
|
||||
&i.HasUnread,
|
||||
@@ -8134,7 +8265,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
|
||||
|
||||
const getStaleChats = `-- name: GetStaleChats :many
|
||||
SELECT
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM
|
||||
chats_expanded
|
||||
WHERE
|
||||
@@ -8195,6 +8326,8 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -8310,7 +8443,7 @@ INSERT INTO chats (
|
||||
$15::jsonb,
|
||||
$16::chat_client_type
|
||||
)
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -8342,13 +8475,16 @@ chats_expanded AS (
|
||||
inserted_chat.plan_mode,
|
||||
inserted_chat.client_type,
|
||||
inserted_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
inserted_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = inserted_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -8420,6 +8556,8 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -8982,7 +9120,7 @@ WITH updated_chats AS (
|
||||
archived = false,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1::uuid OR root_chat_id = $1::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9014,13 +9152,16 @@ chats_expanded AS (
|
||||
updated_chats.plan_mode,
|
||||
updated_chats.client_type,
|
||||
updated_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chats.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
|
||||
`
|
||||
@@ -9067,6 +9208,8 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
); err != nil {
|
||||
@@ -9142,6 +9285,27 @@ func (q *sqlQuerier) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const updateChatACLByID = `-- name: UpdateChatACLByID :exec
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
user_acl = $1,
|
||||
group_acl = $2
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
`
|
||||
|
||||
type UpdateChatACLByIDParams struct {
|
||||
UserACL ChatACL `db:"user_acl" json:"user_acl"`
|
||||
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateChatACLByID(ctx context.Context, arg UpdateChatACLByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateChatACLByID, arg.UserACL, arg.GroupACL, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateChatBuildAgentBinding = `-- name: UpdateChatBuildAgentBinding :one
|
||||
WITH updated_chat AS (
|
||||
UPDATE chats SET
|
||||
@@ -9150,7 +9314,7 @@ UPDATE chats SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9182,13 +9346,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9230,6 +9397,8 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9245,7 +9414,7 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9277,13 +9446,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9324,6 +9496,8 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9384,7 +9558,7 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9416,13 +9590,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9463,6 +9640,8 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9475,7 +9654,7 @@ UPDATE chats SET
|
||||
last_injected_context = $1::jsonb
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9507,13 +9686,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9558,6 +9740,8 @@ func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg Upda
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9573,7 +9757,7 @@ SET
|
||||
last_model_config_id = $1::uuid
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9605,13 +9789,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9652,6 +9839,8 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9719,7 +9908,7 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9751,13 +9940,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -9798,6 +9990,8 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -9931,7 +10125,7 @@ SET
|
||||
plan_mode = $1::chat_plan_mode
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -9963,13 +10157,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -10010,6 +10207,8 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -10029,7 +10228,7 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $6::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -10061,13 +10260,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -10119,6 +10321,8 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -10138,7 +10342,7 @@ SET
|
||||
updated_at = $6::timestamptz
|
||||
WHERE
|
||||
id = $7::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -10170,13 +10374,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -10230,6 +10437,8 @@ func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -10247,7 +10456,7 @@ SET
|
||||
title = $1::text
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -10279,13 +10488,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -10326,6 +10538,8 @@ func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitl
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
@@ -10340,7 +10554,7 @@ UPDATE chats SET
|
||||
agent_id = $3::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE id = $4::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
@@ -10372,13 +10586,16 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, owner_username, owner_name
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
|
||||
FROM chats_expanded
|
||||
`
|
||||
|
||||
@@ -10426,6 +10643,8 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC
|
||||
&i.PlanMode,
|
||||
&i.ClientType,
|
||||
&i.LastTurnSummary,
|
||||
&i.UserACL,
|
||||
&i.GroupACL,
|
||||
&i.OwnerUsername,
|
||||
&i.OwnerName,
|
||||
)
|
||||
|
||||
@@ -35,10 +35,13 @@ chats_expanded AS (
|
||||
updated_chats.plan_mode,
|
||||
updated_chats.client_type,
|
||||
updated_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chats.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -87,10 +90,13 @@ chats_expanded AS (
|
||||
updated_chats.plan_mode,
|
||||
updated_chats.client_type,
|
||||
updated_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chats.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -287,6 +293,24 @@ SELECT *
|
||||
FROM chats_expanded
|
||||
WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetChatACLByID :one
|
||||
SELECT
|
||||
user_acl AS users,
|
||||
group_acl AS groups
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: UpdateChatACLByID :exec
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
user_acl = @user_acl,
|
||||
group_acl = @group_acl
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: GetChatMessageByID :one
|
||||
SELECT
|
||||
*
|
||||
@@ -458,7 +482,11 @@ FROM
|
||||
chats_expanded
|
||||
WHERE
|
||||
CASE
|
||||
WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chats_expanded.owner_id = @owner_id
|
||||
WHEN @owned_only::boolean THEN chats_expanded.owner_id = @viewer_id::uuid
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @shared_only::boolean THEN chats_expanded.owner_id != @viewer_id::uuid
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
@@ -620,10 +648,13 @@ chats_expanded AS (
|
||||
inserted_chat.plan_mode,
|
||||
inserted_chat.client_type,
|
||||
inserted_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
inserted_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = inserted_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -752,10 +783,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -804,10 +838,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -854,10 +891,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -904,10 +944,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -954,10 +997,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1003,10 +1049,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1052,10 +1101,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1103,10 +1155,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1172,10 +1227,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1278,10 +1336,13 @@ chats_expanded AS (
|
||||
acquired_chats.plan_mode,
|
||||
acquired_chats.client_type,
|
||||
acquired_chats.last_turn_summary,
|
||||
COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
acquired_chats
|
||||
LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = acquired_chats.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1332,10 +1393,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1386,10 +1450,13 @@ chats_expanded AS (
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
@@ -1641,14 +1708,33 @@ chats_expanded AS (
|
||||
locked_chat.plan_mode,
|
||||
locked_chat.client_type,
|
||||
locked_chat.last_turn_summary,
|
||||
COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name
|
||||
FROM locked_chat
|
||||
FROM
|
||||
locked_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = locked_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
FROM chats_expanded;
|
||||
|
||||
-- name: GetChatsByChatFileID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats_expanded
|
||||
WHERE
|
||||
id IN (
|
||||
SELECT chat_id
|
||||
FROM chat_file_links
|
||||
WHERE file_id = @file_id::uuid
|
||||
)
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedChatsByChatFileID.
|
||||
-- @authorize_filter
|
||||
;
|
||||
|
||||
-- name: AcquireStaleChatDiffStatuses :many
|
||||
WITH acquired AS (
|
||||
UPDATE
|
||||
|
||||
@@ -71,6 +71,18 @@ sql:
|
||||
- column: "chats_expanded.labels"
|
||||
go_type:
|
||||
type: "StringMap"
|
||||
- column: "chats.user_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "chats.group_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "chats_expanded.user_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "chats_expanded.group_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "users.rbac_roles"
|
||||
go_type: "github.com/lib/pq.StringArray"
|
||||
- column: "templates.user_acl"
|
||||
|
||||
@@ -80,6 +80,41 @@ func (t TemplateACL) Value() (driver.Value, error) {
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
type ChatACL map[string]ChatACLEntry
|
||||
|
||||
func (c *ChatACL) Scan(src interface{}) error {
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), &c)
|
||||
case []byte:
|
||||
return json.Unmarshal(v, &c)
|
||||
case json.RawMessage:
|
||||
return json.Unmarshal(v, &c)
|
||||
}
|
||||
|
||||
return xerrors.Errorf("unexpected type %T", src)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func (c ChatACL) RBACACL() map[string][]policy.Action {
|
||||
rbacACL := make(map[string][]policy.Action, len(c))
|
||||
for id, entry := range c {
|
||||
rbacACL[id] = entry.Permissions
|
||||
}
|
||||
return rbacACL
|
||||
}
|
||||
|
||||
func (c ChatACL) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return json.Marshal(ChatACL{})
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
type ChatACLEntry struct {
|
||||
Permissions []policy.Action `json:"permissions"`
|
||||
}
|
||||
|
||||
type WorkspaceACL map[string]WorkspaceACLEntry
|
||||
|
||||
func (t *WorkspaceACL) Scan(src interface{}) error {
|
||||
|
||||
+32
-9
@@ -364,7 +364,8 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
params := database.GetChatsParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: apiKey.UserID,
|
||||
Archived: searchParams.Archived,
|
||||
AfterID: paginationParams.AfterID,
|
||||
LabelFilter: labelFilter,
|
||||
@@ -2576,6 +2577,11 @@ func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chat := httpmw.ChatParam(r)
|
||||
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.Chat](rw, &audit.RequestParams{
|
||||
Audit: *api.Auditor.Load(),
|
||||
Log: api.Logger,
|
||||
@@ -2891,7 +2897,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
|
||||
// Sending a message triggers LLM inference, requiring update
|
||||
// permission on the org-scoped chat resource.
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.Forbidden(rw)
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3085,6 +3091,11 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
chat := httpmw.ChatParam(r)
|
||||
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Only the chat owner may edit messages. See postChatMessages
|
||||
// for the security rationale.
|
||||
if apiKey.UserID != chat.OwnerID {
|
||||
@@ -3199,6 +3210,11 @@ func (api *API) deleteChatQueuedMessage(rw http.ResponseWriter, r *http.Request)
|
||||
chat := httpmw.ChatParam(r)
|
||||
chatID := chat.ID
|
||||
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
queuedMessageIDStr := chi.URLParam(r, "queuedMessage")
|
||||
queuedMessageID, err := strconv.ParseInt(queuedMessageIDStr, 10, 64)
|
||||
if err != nil {
|
||||
@@ -3238,7 +3254,7 @@ func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request
|
||||
// Promoting a queued message triggers LLM inference,
|
||||
// requiring update permission on the org-scoped chat resource.
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.Forbidden(rw)
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3409,11 +3425,13 @@ func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
go httpapi.HeartbeatClose(ctx, logger, cancel, conn)
|
||||
|
||||
// Mark the chat as read when the stream connects and again
|
||||
// when it disconnects so we avoid per-message API calls while
|
||||
// messages are actively streaming.
|
||||
api.markChatAsRead(ctx, chatID)
|
||||
defer api.markChatAsRead(context.WithoutCancel(ctx), chatID)
|
||||
// The last_read_message_id field is owner-scoped. Shared readers
|
||||
// intentionally lack chat update permission, so their streams must not
|
||||
// update it.
|
||||
if chat.OwnerID == httpmw.APIKey(r).UserID {
|
||||
api.markChatAsRead(ctx, chatID)
|
||||
defer api.markChatAsRead(context.WithoutCancel(ctx), chatID)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(wsNetConn)
|
||||
|
||||
@@ -3499,6 +3517,11 @@ func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
|
||||
chatID := chat.ID
|
||||
logger := api.Logger.Named("chat_interrupt").With(slog.F("chat_id", chatID))
|
||||
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
if api.chatDaemon != nil {
|
||||
chat = api.chatDaemon.InterruptChat(ctx, chat)
|
||||
} else {
|
||||
@@ -8113,7 +8136,7 @@ func (api *API) postChatToolResults(rw http.ResponseWriter, r *http.Request) {
|
||||
// Submitting tool results resumes LLM inference,
|
||||
// requiring update permission on the org-scoped chat resource.
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.Forbidden(rw)
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -14291,6 +14291,233 @@ func requireSDKError(t *testing.T, err error, expectedStatus int) *codersdk.Erro
|
||||
return sdkErr
|
||||
}
|
||||
|
||||
func TestChatReadOnlySharedWriteHandlers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const sharedChatText = "read only shared chat"
|
||||
|
||||
setup := func(t *testing.T) (
|
||||
ctx context.Context,
|
||||
ownerClient *codersdk.ExperimentalClient,
|
||||
sharedClient *codersdk.ExperimentalClient,
|
||||
chat codersdk.Chat,
|
||||
db database.Store,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
ownerClient, db = newChatClientWithDatabase(t)
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient.Client)
|
||||
_ = createChatModelConfig(t, ownerClient)
|
||||
sharedRaw, sharedUser := coderdtest.CreateAnotherUser(
|
||||
t,
|
||||
ownerClient.Client,
|
||||
owner.OrganizationID,
|
||||
rbac.ScopedRoleAgentsAccess(owner.OrganizationID),
|
||||
)
|
||||
sharedClient = codersdk.NewExperimentalClient(sharedRaw)
|
||||
|
||||
var err error
|
||||
chat, err = ownerClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: owner.OrganizationID,
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: sharedChatText,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.UpdateChatACLByID(dbauthz.As(ctx, rbac.Subject{
|
||||
ID: owner.UserID.String(),
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
|
||||
Scope: rbac.ScopeAll,
|
||||
}), database.UpdateChatACLByIDParams{
|
||||
ID: chat.ID,
|
||||
UserACL: database.ChatACL{
|
||||
sharedUser.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
GroupACL: database.ChatACL{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return ctx, ownerClient, sharedClient, chat, db
|
||||
}
|
||||
|
||||
t.Run("GetChatAndMessages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
|
||||
gotChat, err := sharedClient.GetChat(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chat.ID, gotChat.ID)
|
||||
|
||||
messagesResult, err := sharedClient.GetChatMessages(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, messagesResult.Messages)
|
||||
|
||||
foundUserMessage := false
|
||||
for _, message := range messagesResult.Messages {
|
||||
if message.Role != codersdk.ChatMessageRoleUser {
|
||||
continue
|
||||
}
|
||||
for _, part := range message.Content {
|
||||
if part.Type == codersdk.ChatMessagePartTypeText && part.Text == sharedChatText {
|
||||
foundUserMessage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, foundUserMessage)
|
||||
})
|
||||
|
||||
t.Run("PatchChat", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
err := sharedClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
|
||||
Archived: ptr.Ref(true),
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PatchChatMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, ownerClient, sharedClient, chat, _ := setup(t)
|
||||
messagesResult, err := ownerClient.GetChatMessages(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
var userMessageID int64
|
||||
for _, msg := range messagesResult.Messages {
|
||||
if msg.Role == codersdk.ChatMessageRoleUser {
|
||||
userMessageID = msg.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotZero(t, userMessageID)
|
||||
|
||||
_, err = sharedClient.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: "read only user cannot edit",
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PostChatMessages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: "read only user cannot send messages",
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PromoteChatQueuedMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, db := setup(t)
|
||||
queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText("queued"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
queuedMessage, err := db.InsertChatQueuedMessage(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
database.InsertChatQueuedMessageParams{
|
||||
ChatID: chat.ID,
|
||||
Content: queuedContent,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := sharedClient.Request(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/queue/%d/promote", chat.ID, queuedMessage.ID),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("PostChatToolResults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
err := sharedClient.SubmitToolResults(ctx, chat.ID, codersdk.SubmitToolResultsRequest{
|
||||
Results: []codersdk.ToolResult{{
|
||||
ToolCallID: "call_read_only",
|
||||
Output: json.RawMessage(`"forbidden"`),
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteChatQueuedMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, db := setup(t)
|
||||
queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText("queued"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
queuedMessage, err := db.InsertChatQueuedMessage(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
database.InsertChatQueuedMessageParams{
|
||||
ChatID: chat.ID,
|
||||
Content: queuedContent,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := sharedClient.Request(
|
||||
ctx,
|
||||
http.MethodDelete,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/queue/%d", chat.ID, queuedMessage.ID),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InterruptChat", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.InterruptChat(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("RegenerateChatTitle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.RegenerateChatTitle(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("ProposeChatTitle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.ProposeChatTitle(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChatOwnerOnlyWriteHandlers verifies that only the chat owner can
|
||||
// call handlers that trigger chat processing. Org admins pass the RBAC
|
||||
// ActionUpdate check (org-level permission) but must still be blocked
|
||||
|
||||
@@ -708,12 +708,15 @@ func ConfigWithoutACL() regosql.ConvertConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigChats is the configuration for converting rego to SQL when
|
||||
// the target table is "chats", which has no ACL
|
||||
// columns.
|
||||
// ConfigChats uses a resource converter so SQL filters qualify chat
|
||||
// ACL columns consistently with GetChats.
|
||||
func ConfigChats() regosql.ConvertConfig {
|
||||
converter := regosql.ChatConverter()
|
||||
if ChatACLDisabled() {
|
||||
converter = regosql.ChatNoACLConverter()
|
||||
}
|
||||
return regosql.ConvertConfig{
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
VariableConverter: converter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -253,3 +253,17 @@ func SetWorkspaceACLDisabled(v bool) {
|
||||
func WorkspaceACLDisabled() bool {
|
||||
return workspaceACLDisabled.Load()
|
||||
}
|
||||
|
||||
var chatACLDisabled atomic.Bool
|
||||
|
||||
// SetChatACLDisabled is global because database model methods build
|
||||
// RBAC objects without API instance state.
|
||||
func SetChatACLDisabled(v bool) {
|
||||
chatACLDisabled.Store(v)
|
||||
}
|
||||
|
||||
// ChatACLDisabled is global because database model methods build RBAC
|
||||
// objects without API instance state.
|
||||
func ChatACLDisabled() bool {
|
||||
return chatACLDisabled.Load()
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ var (
|
||||
// - "ActionCreate" :: create a new chat
|
||||
// - "ActionDelete" :: delete a chat
|
||||
// - "ActionRead" :: read chat messages and metadata
|
||||
// - "ActionShare" :: share a chat with other users or groups
|
||||
// - "ActionUpdate" :: update chat title or settings
|
||||
ResourceChat = Object{
|
||||
Type: "chat",
|
||||
|
||||
@@ -82,6 +82,7 @@ var chatActions = map[Action]ActionDefinition{
|
||||
ActionRead: "read chat messages and metadata",
|
||||
ActionUpdate: "update chat title or settings",
|
||||
ActionDelete: "delete a chat",
|
||||
ActionShare: "share a chat with other users or groups",
|
||||
}
|
||||
|
||||
// RBACPermissions is indexed by the type
|
||||
|
||||
@@ -217,6 +217,16 @@ func TestRegoQueries(t *testing.T) {
|
||||
" OR (workspaces.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.WorkspaceConverter(),
|
||||
},
|
||||
{
|
||||
Name: "UserChatACLAllow",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
`"*" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
},
|
||||
ExpectedSQL: "((chats_expanded.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? 'read')" +
|
||||
" OR (chats_expanded.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
},
|
||||
{
|
||||
Name: "NoACLConfig",
|
||||
Queries: []string{
|
||||
|
||||
@@ -50,6 +50,34 @@ func WorkspaceConverter() *sqltypes.VariableConverter {
|
||||
return matcher
|
||||
}
|
||||
|
||||
func ChatConverter() *sqltypes.VariableConverter {
|
||||
matcher := chatBaseConverter()
|
||||
matcher.RegisterMatcher(
|
||||
ACLMappingMatcher(matcher, "chats_expanded.group_acl", []string{"input", "object", "acl_group_list"}).UsingSubfield("permissions"),
|
||||
ACLMappingMatcher(matcher, "chats_expanded.user_acl", []string{"input", "object", "acl_user_list"}).UsingSubfield("permissions"),
|
||||
)
|
||||
|
||||
return matcher
|
||||
}
|
||||
|
||||
func ChatNoACLConverter() *sqltypes.VariableConverter {
|
||||
matcher := chatBaseConverter()
|
||||
matcher.RegisterMatcher(
|
||||
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
||||
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
||||
)
|
||||
|
||||
return matcher
|
||||
}
|
||||
|
||||
func chatBaseConverter() *sqltypes.VariableConverter {
|
||||
return sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
sqltypes.StringVarMatcher("chats_expanded.organization_id :: text", []string{"input", "object", "org_owner"}),
|
||||
userOwnerMatcher(),
|
||||
)
|
||||
}
|
||||
|
||||
func AuditLogConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
|
||||
+11
-4
@@ -243,6 +243,7 @@ var builtInRoles map[string]func(orgID uuid.UUID) Role
|
||||
type RoleOptions struct {
|
||||
NoOwnerWorkspaceExec bool
|
||||
NoWorkspaceSharing bool
|
||||
NoChatSharing bool
|
||||
}
|
||||
|
||||
// ReservedRoleName exists because the database should only allow unique role
|
||||
@@ -272,6 +273,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
Action: policy.ActionShare,
|
||||
})
|
||||
}
|
||||
if opts.NoChatSharing {
|
||||
denyPermissions = append(denyPermissions, Permission{
|
||||
Negate: true,
|
||||
ResourceType: ResourceChat.Type,
|
||||
Action: policy.ActionShare,
|
||||
})
|
||||
}
|
||||
|
||||
ownerWorkspaceActions := ResourceWorkspace.AvailableActions()
|
||||
if opts.NoOwnerWorkspaceExec {
|
||||
@@ -589,10 +597,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
},
|
||||
}
|
||||
},
|
||||
// agentsAccess grants org members permission to create, read, and
|
||||
// update chats. ActionDelete is intentionally excluded: no dbauthz
|
||||
// function checks it on ResourceChat. Hard-deletion goes through
|
||||
// ResourceSystem (dbpurge).
|
||||
// ActionDelete is intentionally excluded because hard-deletion goes through
|
||||
// ResourceSystem in dbpurge.
|
||||
agentsAccess: func(organizationID uuid.UUID) Role {
|
||||
return Role{
|
||||
Identifier: RoleIdentifier{Name: agentsAccess, OrganizationID: organizationID},
|
||||
@@ -606,6 +612,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceChat.Type: {
|
||||
policy.ActionCreate,
|
||||
policy.ActionRead,
|
||||
policy.ActionShare,
|
||||
policy.ActionUpdate,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -115,6 +115,58 @@ func TestOrgSharingPermissions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestChatSharingPermissions(t *testing.T) {
|
||||
target := rbac.Permission{
|
||||
Negate: true,
|
||||
ResourceType: rbac.ResourceChat.Type,
|
||||
Action: policy.ActionShare,
|
||||
}
|
||||
orgID := uuid.New()
|
||||
userID := uuid.NewString()
|
||||
resource := rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(userID)
|
||||
|
||||
authorizeAgentsAccessUser := func(t *testing.T) error {
|
||||
t.Helper()
|
||||
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID))
|
||||
require.NoError(t, err)
|
||||
|
||||
auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
|
||||
return auth.Authorize(context.Background(), rbac.Subject{
|
||||
ID: userID,
|
||||
Roles: rbac.Roles{memberRole, agentsRole},
|
||||
Scope: rbac.ScopeAll,
|
||||
}, policy.ActionShare, resource)
|
||||
}
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
rbac.ReloadBuiltinRoles(nil)
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, permissionGranted(memberRole.Site, target))
|
||||
require.NoError(t, authorizeAgentsAccessUser(t))
|
||||
})
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) {
|
||||
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
|
||||
NoChatSharing: true,
|
||||
})
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, permissionGranted(memberRole.Site, target))
|
||||
|
||||
err = authorizeAgentsAccessUser(t)
|
||||
require.ErrorAs(t, err, &rbac.UnauthorizedError{})
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestOwnerExec(t *testing.T) {
|
||||
owner := rbac.Subject{
|
||||
@@ -1158,6 +1210,15 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatUsageShare",
|
||||
Actions: []policy.Action{policy.ActionShare},
|
||||
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatUsageDelete",
|
||||
Actions: []policy.Action{policy.ActionDelete},
|
||||
|
||||
@@ -39,6 +39,7 @@ const (
|
||||
ScopeChatCreate ScopeName = "chat:create"
|
||||
ScopeChatDelete ScopeName = "chat:delete"
|
||||
ScopeChatRead ScopeName = "chat:read"
|
||||
ScopeChatShare ScopeName = "chat:share"
|
||||
ScopeChatUpdate ScopeName = "chat:update"
|
||||
ScopeConnectionLogRead ScopeName = "connection_log:read"
|
||||
ScopeConnectionLogUpdate ScopeName = "connection_log:update"
|
||||
@@ -211,6 +212,7 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeChatCreate,
|
||||
ScopeChatDelete,
|
||||
ScopeChatRead,
|
||||
ScopeChatShare,
|
||||
ScopeChatUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
@@ -384,6 +386,7 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeChatCreate,
|
||||
ScopeChatDelete,
|
||||
ScopeChatRead,
|
||||
ScopeChatShare,
|
||||
ScopeChatUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -74,3 +75,85 @@ func TestActiveAgentChatDefinitionsAgree(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveAgentChatsIncludeInheritedACLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitMedium))
|
||||
db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
||||
|
||||
org, err := db.GetDefaultOrganization(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
owner := dbgen.User(t, db, database.User{})
|
||||
workspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: owner.ID,
|
||||
}).WithAgent().Do()
|
||||
modelConfig := insertAgentChatTestModelConfig(t, db, owner.ID)
|
||||
|
||||
root, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OrganizationID: org.ID,
|
||||
Status: database.ChatStatusWaiting,
|
||||
ClientType: database.ChatClientTypeUi,
|
||||
OwnerID: owner.ID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "root-active-chat",
|
||||
AgentID: uuid.NullUUID{UUID: workspace.Agents[0].ID, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
child, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OrganizationID: org.ID,
|
||||
Status: database.ChatStatusRunning,
|
||||
ClientType: database.ChatClientTypeUi,
|
||||
OwnerID: owner.ID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "child-active-chat",
|
||||
AgentID: uuid.NullUUID{UUID: workspace.Agents[0].ID, Valid: true},
|
||||
ParentChatID: uuid.NullUUID{UUID: root.ID, Valid: true},
|
||||
RootChatID: uuid.NullUUID{UUID: root.ID, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rootUserACL := database.ChatACL{
|
||||
owner.ID.String(): {Permissions: []policy.Action{policy.ActionRead, policy.ActionSSH}},
|
||||
}
|
||||
rootGroupACL := database.ChatACL{
|
||||
org.ID.String(): {Permissions: []policy.Action{policy.ActionRead}},
|
||||
}
|
||||
|
||||
userACLValue, err := rootUserACL.Value()
|
||||
require.NoError(t, err)
|
||||
groupACLValue, err := rootGroupACL.Value()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sqlDB.ExecContext(
|
||||
ctx,
|
||||
`UPDATE chats SET user_acl = $1::jsonb, group_acl = $2::jsonb WHERE id = $3`,
|
||||
userACLValue,
|
||||
groupACLValue,
|
||||
root.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
activeChats, err := db.GetActiveChatsByAgentID(ctx, workspace.Agents[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activeChats, 2)
|
||||
|
||||
activeByID := make(map[uuid.UUID]database.Chat, len(activeChats))
|
||||
for _, chat := range activeChats {
|
||||
activeByID[chat.ID] = chat
|
||||
}
|
||||
|
||||
fetchedRoot, ok := activeByID[root.ID]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, rootUserACL, fetchedRoot.UserACL)
|
||||
require.Equal(t, rootGroupACL, fetchedRoot.GroupACL)
|
||||
|
||||
fetchedChild, ok := activeByID[child.ID]
|
||||
require.True(t, ok)
|
||||
require.True(t, fetchedChild.ParentChatID.Valid)
|
||||
require.Equal(t, rootUserACL, fetchedChild.UserACL)
|
||||
require.Equal(t, rootGroupACL, fetchedChild.GroupACL)
|
||||
}
|
||||
|
||||
@@ -721,7 +721,10 @@ func TestExploreSubagentIsReadOnly(t *testing.T) {
|
||||
require.True(t, requestHasSystemSubstring(childRequests[0], "You are in Explore Mode as a delegated sub-agent."))
|
||||
require.False(t, requestHasSystemSubstring(rootRequests[0], "You are in Explore Mode as a delegated sub-agent."))
|
||||
|
||||
rootChats, err := db.GetChats(dbauthz.AsChatd(ctx), database.GetChatsParams{OwnerID: user.UserID})
|
||||
rootChats, err := db.GetChats(dbauthz.AsChatd(ctx), database.GetChatsParams{
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
rootIDs := make([]uuid.UUID, 0, len(rootChats))
|
||||
for _, root := range rootChats {
|
||||
@@ -2362,7 +2365,8 @@ func TestCreateChatRejectsWhenUsageLimitReached(t *testing.T) {
|
||||
})
|
||||
|
||||
beforeChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.ID,
|
||||
AfterID: uuid.Nil,
|
||||
OffsetOpt: 0,
|
||||
LimitOpt: 100,
|
||||
@@ -2385,7 +2389,8 @@ func TestCreateChatRejectsWhenUsageLimitReached(t *testing.T) {
|
||||
require.Equal(t, int64(100), limitErr.ConsumedMicros)
|
||||
|
||||
afterChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.ID,
|
||||
AfterID: uuid.Nil,
|
||||
OffsetOpt: 0,
|
||||
LimitOpt: 100,
|
||||
|
||||
@@ -2005,7 +2005,8 @@ func TestSpawnAgent_ComputerUseRejectsMissingConfiguredProvider(t *testing.T) {
|
||||
ids := availableSubagentTypeIDs(ctx, server, parentChat)
|
||||
require.Contains(t, ids, subagentTypeComputerUse)
|
||||
beforeChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.ID,
|
||||
AfterID: uuid.Nil,
|
||||
OffsetOpt: 0,
|
||||
LimitOpt: 100,
|
||||
@@ -2021,7 +2022,8 @@ func TestSpawnAgent_ComputerUseRejectsMissingConfiguredProvider(t *testing.T) {
|
||||
require.Contains(t, resp.Content, "computer-use")
|
||||
require.Contains(t, resp.Content, "openai")
|
||||
afterChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
OwnedOnly: true,
|
||||
ViewerID: user.ID,
|
||||
AfterID: uuid.Nil,
|
||||
OffsetOpt: 0,
|
||||
LimitOpt: 100,
|
||||
|
||||
Reference in New Issue
Block a user