Implement private tag filtering for event visibility

- Added functionality to filter events based on "private" tags, allowing only authorized users to see private events.
- Introduced a new method `canSeePrivateEvent` to check user permissions against private tags.
- Updated event delivery logic to deny access to unauthorized users for private events, enhancing security and user experience.
- Bumped version to v0.17.7.
This commit is contained in:
2025-10-21 19:17:16 +01:00
parent 8609e9dc22
commit da66e26614
4 changed files with 174 additions and 62 deletions

View File

@@ -431,6 +431,44 @@ privCheck:
allEvents = aclFilteredEvents allEvents = aclFilteredEvents
} }
// Apply private tag filtering - only show events with "private" tags to authorized users
var privateFilteredEvents event.S
authedPubkey := l.authedPubkey.Load()
for _, ev := range allEvents {
// Check if event has private tags
hasPrivateTag := false
var privatePubkey []byte
if ev.Tags != nil && ev.Tags.Len() > 0 {
for _, t := range *ev.Tags {
if t.Len() >= 2 {
keyBytes := t.Key()
if len(keyBytes) == 7 && string(keyBytes) == "private" {
hasPrivateTag = true
privatePubkey = t.Value()
break
}
}
}
}
// If no private tag, include the event
if !hasPrivateTag {
privateFilteredEvents = append(privateFilteredEvents, ev)
continue
}
// Event has private tag - check if user is authorized to see it
canSeePrivate := l.canSeePrivateEvent(authedPubkey, privatePubkey)
if canSeePrivate {
privateFilteredEvents = append(privateFilteredEvents, ev)
log.D.F("private tag: allowing event %s for authorized user", hexenc.Enc(ev.ID))
} else {
log.D.F("private tag: filtering out event %s from unauthorized user", hexenc.Enc(ev.ID))
}
}
allEvents = privateFilteredEvents
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, ev := range allEvents { for _, ev := range allEvents {
log.T.C( log.T.C(

View File

@@ -12,6 +12,7 @@ import (
"next.orly.dev/pkg/database" "next.orly.dev/pkg/database"
"next.orly.dev/pkg/encoders/event" "next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/filter" "next.orly.dev/pkg/encoders/filter"
"next.orly.dev/pkg/utils"
"next.orly.dev/pkg/utils/atomic" "next.orly.dev/pkg/utils/atomic"
) )
@@ -133,3 +134,25 @@ func (l *Listener) QueryEvents(ctx context.Context, f *filter.F) (event.S, error
func (l *Listener) QueryAllVersions(ctx context.Context, f *filter.F) (event.S, error) { func (l *Listener) QueryAllVersions(ctx context.Context, f *filter.F) (event.S, error) {
return l.D.QueryAllVersions(ctx, f) return l.D.QueryAllVersions(ctx, f)
} }
// canSeePrivateEvent checks if the authenticated user can see an event with a private tag
func (l *Listener) canSeePrivateEvent(authedPubkey, privatePubkey []byte) (canSee bool) {
// If no authenticated user, deny access
if len(authedPubkey) == 0 {
return false
}
// If the authenticated user matches the private tag pubkey, allow access
if len(privatePubkey) > 0 && utils.FastEqual(authedPubkey, privatePubkey) {
return true
}
// Check if user is an admin or owner (they can see all private events)
accessLevel := acl.Registry.GetAccessLevel(authedPubkey, l.remote)
if accessLevel == "admin" || accessLevel == "owner" {
return true
}
// Default deny
return false
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"lol.mleku.dev/chk" "lol.mleku.dev/chk"
"lol.mleku.dev/log" "lol.mleku.dev/log"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/encoders/envelopes/eventenvelope" "next.orly.dev/pkg/encoders/envelopes/eventenvelope"
"next.orly.dev/pkg/encoders/event" "next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/filter" "next.orly.dev/pkg/encoders/filter"
@@ -211,68 +212,96 @@ func (p *P) Deliver(ev *event.E) {
break break
} }
} }
} }
if !allowed { if !allowed {
log.D.F("subscription delivery DENIED for privileged event %s to %s (auth mismatch)", log.D.F("subscription delivery DENIED for privileged event %s to %s (auth mismatch)",
hex.Enc(ev.ID), d.sub.remote) hex.Enc(ev.ID), d.sub.remote)
// Skip delivery for this subscriber // Skip delivery for this subscriber
continue continue
} }
} }
var res *eventenvelope.Result // Check for private tags - only deliver to authorized users
if res, err = eventenvelope.NewResultWith(d.id, ev); chk.E(err) { if ev.Tags != nil && ev.Tags.Len() > 0 {
log.E.F("failed to create event envelope for %s to %s: %v", hasPrivateTag := false
hex.Enc(ev.ID), d.sub.remote, err) var privatePubkey []byte
continue
}
// Log delivery attempt for _, t := range *ev.Tags {
msgData := res.Marshal(nil) if t.Len() >= 2 {
log.D.F("attempting delivery of event %s (kind=%d, len=%d) to subscription %s @ %s", keyBytes := t.Key()
hex.Enc(ev.ID), ev.Kind, len(msgData), d.id, d.sub.remote) if len(keyBytes) == 7 && string(keyBytes) == "private" {
hasPrivateTag = true
privatePubkey = t.Value()
break
}
}
}
// Use a separate context with timeout for writes to prevent race conditions if hasPrivateTag {
// where the publisher context gets cancelled while writing events canSeePrivate := p.canSeePrivateEvent(d.sub.AuthedPubkey, privatePubkey, d.sub.remote)
writeCtx, cancel := context.WithTimeout( if !canSeePrivate {
context.Background(), DefaultWriteTimeout, log.D.F("subscription delivery DENIED for private event %s to %s (unauthorized)",
) hex.Enc(ev.ID), d.sub.remote)
defer cancel() continue
}
log.D.F("subscription delivery ALLOWED for private event %s to %s (authorized)",
hex.Enc(ev.ID), d.sub.remote)
}
}
deliveryStart := time.Now() var res *eventenvelope.Result
if err = d.w.Write( if res, err = eventenvelope.NewResultWith(d.id, ev); chk.E(err) {
writeCtx, websocket.MessageText, msgData, log.E.F("failed to create event envelope for %s to %s: %v",
); err != nil { hex.Enc(ev.ID), d.sub.remote, err)
deliveryDuration := time.Since(deliveryStart) continue
}
// Log detailed failure information // Log delivery attempt
log.E.F("subscription delivery FAILED: event=%s to=%s sub=%s duration=%v error=%v", msgData := res.Marshal(nil)
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, err) log.D.F("attempting delivery of event %s (kind=%d, len=%d) to subscription %s @ %s",
hex.Enc(ev.ID), ev.Kind, len(msgData), d.id, d.sub.remote)
// Check for timeout specifically // Use a separate context with timeout for writes to prevent race conditions
if writeCtx.Err() != nil { // where the publisher context gets cancelled while writing events
log.E.F("subscription delivery TIMEOUT: event=%s to=%s after %v (limit=%v)", writeCtx, cancel := context.WithTimeout(
hex.Enc(ev.ID), d.sub.remote, deliveryDuration, DefaultWriteTimeout) context.Background(), DefaultWriteTimeout,
} )
defer cancel()
// Log connection cleanup deliveryStart := time.Now()
log.D.F("removing failed subscriber connection: %s", d.sub.remote) if err = d.w.Write(
writeCtx, websocket.MessageText, msgData,
); err != nil {
deliveryDuration := time.Since(deliveryStart)
// On error, remove the subscriber connection safely // Log detailed failure information
p.removeSubscriber(d.w) log.E.F("subscription delivery FAILED: event=%s to=%s sub=%s duration=%v error=%v",
_ = d.w.CloseNow() hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, err)
continue
}
deliveryDuration := time.Since(deliveryStart) // Check for timeout specifically
log.D.F("subscription delivery SUCCESS: event=%s to=%s sub=%s duration=%v len=%d", if writeCtx.Err() != nil {
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, len(msgData)) log.E.F("subscription delivery TIMEOUT: event=%s to=%s after %v (limit=%v)",
hex.Enc(ev.ID), d.sub.remote, deliveryDuration, DefaultWriteTimeout)
}
// Log slow deliveries for performance monitoring // Log connection cleanup
if deliveryDuration > time.Millisecond*50 { log.D.F("removing failed subscriber connection: %s", d.sub.remote)
log.D.F("SLOW subscription delivery: event=%s to=%s duration=%v (>50ms)",
hex.Enc(ev.ID), d.sub.remote, deliveryDuration) // On error, remove the subscriber connection safely
} p.removeSubscriber(d.w)
_ = d.w.CloseNow()
continue
}
deliveryDuration := time.Since(deliveryStart)
log.D.F("subscription delivery SUCCESS: event=%s to=%s sub=%s duration=%v len=%d",
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, len(msgData))
// Log slow deliveries for performance monitoring
if deliveryDuration > time.Millisecond*50 {
log.D.F("SLOW subscription delivery: event=%s to=%s duration=%v (>50ms)",
hex.Enc(ev.ID), d.sub.remote, deliveryDuration)
}
} }
} }
@@ -299,3 +328,25 @@ func (p *P) removeSubscriber(ws *websocket.Conn) {
clear(p.Map[ws]) clear(p.Map[ws])
delete(p.Map, ws) delete(p.Map, ws)
} }
// canSeePrivateEvent checks if the authenticated user can see an event with a private tag
func (p *P) canSeePrivateEvent(authedPubkey, privatePubkey []byte, remote string) (canSee bool) {
// If no authenticated user, deny access
if len(authedPubkey) == 0 {
return false
}
// If the authenticated user matches the private tag pubkey, allow access
if len(privatePubkey) > 0 && utils.FastEqual(authedPubkey, privatePubkey) {
return true
}
// Check if user is an admin or owner (they can see all private events)
accessLevel := acl.Registry.GetAccessLevel(authedPubkey, remote)
if accessLevel == "admin" || accessLevel == "owner" {
return true
}
// Default deny
return false
}

View File

@@ -1 +1 @@
v0.17.6 v0.17.7