Authorize data sync on connected agent registration

This commit is contained in:
2026-09-22 16:47:52 +08:00
parent 49c07cca30
commit e26535781d
17 changed files with 194 additions and 110 deletions
+13 -6
View File
@@ -9,6 +9,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
@@ -738,10 +739,13 @@ func (s *AccountStore) QueryConversations(ctx context.Context, limit int, cursor
if err != nil {
return nil, err
}
allChats := slices.ContainsFunc(scopes, func(scope ReportingScope) bool {
return scope.ChatID == "*" && scopeAllows(scopes, "*", "conversations")
})
allowedChats := make([]string, 0, len(scopes))
seenChats := make(map[string]struct{}, len(scopes))
for _, scope := range scopes {
if !scopeAllows(scopes, scope.ChatID, "conversations") {
if allChats || !scopeAllows(scopes, scope.ChatID, "conversations") {
continue
}
if _, exists := seenChats[scope.ChatID]; exists {
@@ -750,15 +754,18 @@ func (s *AccountStore) QueryConversations(ctx context.Context, limit int, cursor
seenChats[scope.ChatID] = struct{}{}
allowedChats = append(allowedChats, scope.ChatID)
}
if len(allowedChats) == 0 {
if !allChats && len(allowedChats) == 0 {
return nil, ErrAccountNotAuthorized
}
const sortExpression = "COALESCE(last_activity_at, observed_at)"
query := `SELECT chat_id, chat_type, title, last_activity_at, source, observed_at, directory_state FROM conversations WHERE chat_id IN (` + strings.TrimSuffix(strings.Repeat("?,", len(allowedChats)), ",") + ")"
query := `SELECT chat_id, chat_type, title, last_activity_at, source, observed_at, directory_state FROM conversations`
args := make([]any, 0, len(allowedChats)+4)
for _, chatID := range allowedChats {
args = append(args, chatID)
if !allChats {
query += ` WHERE chat_id IN (` + strings.TrimSuffix(strings.Repeat("?,", len(allowedChats)), ",") + ")"
for _, chatID := range allowedChats {
args = append(args, chatID)
}
}
if cursor != nil {
query += " AND (" + sortExpression + " < ? OR (" + sortExpression + " = ? AND chat_id > ?))"
@@ -1101,7 +1108,7 @@ func authorizeBatch(batch IngestBatch, scopes []ReportingScope) error {
func scopeAllows(scopes []ReportingScope, chatID, dataType string) bool {
now := time.Now().UTC()
for _, scope := range scopes {
if scope.ChatID != chatID || (scope.ExpiresAt != nil && !scope.ExpiresAt.After(now)) {
if scope.ChatID != "*" && scope.ChatID != chatID || (scope.ExpiresAt != nil && !scope.ExpiresAt.After(now)) {
continue
}
if scope.DataType == "*" || scope.DataType == "read" || scope.DataType == dataType {
+38
View File
@@ -273,6 +273,44 @@ func TestAccountStoreMaintenanceCapacityAndRevocation(t *testing.T) {
}
}
func TestAccountStoreConnectionWildcardScopeAuthorizesAllChats(t *testing.T) {
ctx := context.Background()
manager, err := OpenAccountStoreManager(t.TempDir(), AccountStoreManagerOptions{})
if err != nil {
t.Fatal(err)
}
defer manager.Close()
store, err := manager.RegisterAccount(ctx, AccountRegistration{
AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true,
ReportingScopes: []ReportingScope{{ChatID: "*", DataType: "*", ConfigVersion: 1}},
})
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
if _, err := store.ApplyBatch(ctx, IngestBatch{
BatchID: "wildcard-batch", SourceGeneration: "generation-a", StreamKey: "messages", Sequence: 1, PayloadHash: "wildcard-hash",
Conversations: []ConversationRecord{
{ChatID: "chat-a", ChatType: "private", Title: "a", Source: "db", ObservedAt: now, DirectoryState: "visible"},
{ChatID: "chat-b", ChatType: "private", Title: "b", Source: "db", ObservedAt: now, DirectoryState: "visible"},
},
Messages: []MessageRecord{
{MessageID: "message-a", ChatID: "chat-a", SourceMessageID: "a", Direction: "incoming", MessageType: "text", Text: "a", SourceTime: now, ObservedAt: now, SourceVersion: "wx", PayloadHash: "message-a-hash"},
{MessageID: "message-b", ChatID: "chat-b", SourceMessageID: "b", Direction: "incoming", MessageType: "text", Text: "b", SourceTime: now, ObservedAt: now, SourceVersion: "wx", PayloadHash: "message-b-hash"},
},
}); err != nil {
t.Fatal(err)
}
conversations, err := store.QueryConversations(ctx, 20, nil)
if err != nil || len(conversations) != 2 {
t.Fatalf("wildcard conversation query failed: count=%d err=%v", len(conversations), err)
}
messages, err := store.QueryMessages(ctx, "chat-b", 20, nil)
if err != nil || len(messages) != 1 || messages[0].ChatID != "chat-b" {
t.Fatalf("wildcard message query failed: %+v err=%v", messages, err)
}
}
func TestAccountStoreSyntheticConcurrentReadWrite(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
+17 -5
View File
@@ -97,13 +97,25 @@ func (s *Server) registerDataAccounts(nodeID string, registration NodeRegistrati
if !account.Verified {
continue
}
scopes := make([]ReportingScope, 0, len(account.AllowedChats))
// A wildcard scope is the connection-authorized form. Keep explicit scopes for
// legacy/test registrations that have not opted into connection-wide authorization.
hasWildcard := false
for _, chat := range account.AllowedChats {
dataType := "read"
if chat.ChatID == "" || !validChatType(chat.ChatType) {
continue
if chat.ChatID == "*" {
hasWildcard = true
break
}
}
scopes := make([]ReportingScope, 0, len(account.AllowedChats))
if hasWildcard {
scopes = append(scopes, ReportingScope{ChatID: "*", DataType: "*", ConfigVersion: int(registration.ReportingConfigVersion)})
} else {
for _, chat := range account.AllowedChats {
if chat.ChatID == "" || !validChatType(chat.ChatType) {
continue
}
scopes = append(scopes, ReportingScope{ChatID: chat.ChatID, DataType: "read", ConfigVersion: int(registration.ReportingConfigVersion)})
}
scopes = append(scopes, ReportingScope{ChatID: chat.ChatID, DataType: dataType, ConfigVersion: int(registration.ReportingConfigVersion)})
}
store, err := s.accountStores.RegisterAccount(context.Background(), AccountRegistration{
AccountID: account.AccountID,