Authorize data sync on connected agent registration
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user