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:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
155
app/publisher.go
155
app/publisher.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v0.17.6
|
v0.17.7
|
||||||
Reference in New Issue
Block a user