fix(realtime): scope events to authorized users, authorize rooms, keep SSE alive
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m6s

Phase 0 of the collaboration plan:
- Remove BroadcastToAll; task, task list and workspace folder events now
  only reach users who can see the resource. Task list events carry only
  the list ID, clients reload with their own permissions.
- Reject unauthenticated WebSocket/SSE connections (401). Every room join
  (URL room, JOIN_ROOM, SSE rooms) is checked via a room authorizer;
  /events/rooms requires login and only accepts the caller's own sseId.
- Reorder rejects tasks the caller cannot access or edit (IDOR).
- Lift the server WriteTimeout for SSE streams so they are not cut after 30s.
- Fix folder share removal sending DOC_DELETED to the share ID.
- Tests for all of the above; docs and flaw list updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 11:19:14 +01:00
1 parent 74797e3f44
commit bcc06da3ee
10 files changed
+526 -86

No files matched your search

+49
View File
@@ -4747,6 +4747,55 @@ func (s *Store) GetTaskListShares(taskListID string) ([]domain.TaskListShare, er
return shares, nil
}
// queryUserIDs runs a query returning a single user_id column and collects the distinct IDs.
func (s *Store) queryUserIDs(query string, args ...interface{}) []string {
rows, err := s.db.Query(query, args...)
if err != nil {
return []string{}
}
defer rows.Close()
ids := make([]string, 0)
seen := make(map[string]bool)
for rows.Next() {
var id string
if rows.Scan(&id) == nil && id != "" && !seen[id] {
seen[id] = true
ids = append(ids, id)
}
}
return ids
}
// GetTaskListUserIDs returns every user who can see the task list: owner, direct shares, members of shared teams
// and members of the legacy team_id.
func (s *Store) GetTaskListUserIDs(listID string) []string {
if listID == "" {
return []string{}
}
return s.queryUserIDs(
`SELECT user_id FROM task_lists WHERE id = ?
UNION SELECT target_id FROM task_list_shares WHERE task_list_id = ? AND target_type = 'user'
UNION SELECT tm.user_id FROM team_members tm
WHERE tm.team_id IN (SELECT target_id FROM task_list_shares WHERE task_list_id = ? AND target_type = 'team')
OR tm.team_id IN (SELECT team_id FROM task_lists WHERE id = ? AND team_id != '')`,
listID, listID, listID, listID,
)
}
// GetSharedFolderUserIDs returns the owner of a shared workspace folder and everyone it is shared with.
func (s *Store) GetSharedFolderUserIDs(folderID string) []string {
if folderID == "" {
return []string{}
}
return s.queryUserIDs(
`SELECT owner_id FROM workspace_shared_folders WHERE id = ?
UNION SELECT target_id FROM workspace_folder_shares WHERE folder_id = ? AND target_type = 'user'
UNION SELECT tm.user_id FROM team_members tm
WHERE tm.team_id IN (SELECT target_id FROM workspace_folder_shares WHERE folder_id = ? AND target_type = 'team')`,
folderID, folderID, folderID,
)
}
func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMember {
memberMap := make(map[string]domain.TaskListMember)