mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
feat: SSE event permission filtering
Filter SSE events by the connected user's collection visibility so events about items in hidden collections are never sent (TASK-487). - Compute visible collection slug set at SSE connection time - Filter live events: check event.Collection against visible set - Filter replayed events: same check on missed event replay - Events without a collection field always pass through - Admins and "all access" members see everything (nil set = no filter)
This commit is contained in:
@@ -75,6 +75,33 @@ func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the user's visible collection set for event filtering.
|
||||
// Build a slug-based set since events carry collection slugs, not IDs.
|
||||
var visibleSlugSet map[string]bool // nil = all access (no filtering)
|
||||
visibleIDs, err := s.visibleCollectionIDs(r, ws.ID)
|
||||
if err != nil {
|
||||
slog.Warn("SSE: failed to resolve visible collections, allowing all", "error", err)
|
||||
} else if visibleIDs != nil {
|
||||
visibleSlugSet = make(map[string]bool, len(visibleIDs))
|
||||
for _, id := range visibleIDs {
|
||||
coll, _ := s.store.GetCollection(id)
|
||||
if coll != nil {
|
||||
visibleSlugSet[coll.Slug] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sseEventVisible checks if an event should be sent to this client.
|
||||
sseEventVisible := func(collection string) bool {
|
||||
if visibleSlugSet == nil {
|
||||
return true // all access
|
||||
}
|
||||
if collection == "" {
|
||||
return true // events without a collection are always sent
|
||||
}
|
||||
return visibleSlugSet[collection]
|
||||
}
|
||||
|
||||
// Send initial connected event
|
||||
writeSSEEvent(w, "connected", 0, map[string]string{
|
||||
"workspace_id": ws.ID,
|
||||
@@ -100,8 +127,10 @@ func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Info("SSE replaying missed events",
|
||||
"workspace", ws.Slug, "last_event_id", lastID, "count", len(missed))
|
||||
for _, event := range missed {
|
||||
writeSSEEvent(w, event.Type, event.ID, event)
|
||||
flusher.Flush()
|
||||
if sseEventVisible(event.Collection) {
|
||||
writeSSEEvent(w, event.Type, event.ID, event)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
// If len(missed) == 0: client is caught up, nothing to replay.
|
||||
@@ -124,8 +153,10 @@ func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
// Channel closed (unsubscribed)
|
||||
return
|
||||
}
|
||||
writeSSEEvent(w, event.Type, event.ID, event)
|
||||
flusher.Flush()
|
||||
if sseEventVisible(event.Collection) {
|
||||
writeSSEEvent(w, event.Type, event.ID, event)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
case <-keepalive.C:
|
||||
// Send keepalive comment to prevent proxy/LB timeouts
|
||||
|
||||
Reference in New Issue
Block a user