diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e81c9fc..fbae76c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -109,7 +109,8 @@ "Bash(timeout 30 sh:*)", "Bash(timeout 60 go test:*)", "Bash(timeout 120 go test:*)", - "Bash(timeout 180 ./scripts/test.sh:*)" + "Bash(timeout 180 ./scripts/test.sh:*)", + "Bash(CGO_ENABLED=0 timeout 60 go test:*)" ], "deny": [], "ask": [] diff --git a/app/handle-req.go b/app/handle-req.go index d13fa5a..4b7b0cc 100644 --- a/app/handle-req.go +++ b/app/handle-req.go @@ -24,6 +24,7 @@ import ( "next.orly.dev/pkg/encoders/kind" "next.orly.dev/pkg/encoders/reason" "next.orly.dev/pkg/encoders/tag" + "next.orly.dev/pkg/policy" "next.orly.dev/pkg/protocol/nip43" "next.orly.dev/pkg/utils" "next.orly.dev/pkg/utils/normalize" @@ -360,59 +361,23 @@ func (l *Listener) HandleReq(msg []byte) (err error) { }, ) pk := l.authedPubkey.Load() - if pk == nil { - // Not authenticated - cannot see privileged events + + // Use centralized IsPartyInvolved function for consistent privilege checking + if policy.IsPartyInvolved(ev, pk) { log.T.C( func() string { return fmt.Sprintf( - "privileged event %s denied - not authenticated", - ev.ID, - ) - }, - ) - continue - } - // Check if user is authorized to see this privileged event - authorized := false - if utils.FastEqual(ev.Pubkey, pk) { - authorized = true - log.T.C( - func() string { - return fmt.Sprintf( - "privileged event %s is for logged in pubkey %0x", + "privileged event %s allowed for logged in pubkey %0x", ev.ID, pk, ) }, ) - } else { - // Check p tags - pTags := ev.Tags.GetAll([]byte("p")) - for _, pTag := range pTags { - var pt []byte - if pt, err = hexenc.Dec(string(pTag.Value())); chk.E(err) { - continue - } - if utils.FastEqual(pt, pk) { - authorized = true - log.T.C( - func() string { - return fmt.Sprintf( - "privileged event %s is for logged in pubkey %0x", - ev.ID, pk, - ) - }, - ) - break - } - } - } - if authorized { tmp = append(tmp, ev) } else { log.T.C( func() string { return fmt.Sprintf( - "privileged event %s does not contain the logged in pubkey %0x", + "privileged event %s denied for pubkey %0x (not authenticated or not a party involved)", ev.ID, pk, ) }, diff --git a/app/publisher.go b/app/publisher.go index f3fac1b..62c9998 100644 --- a/app/publisher.go +++ b/app/publisher.go @@ -15,6 +15,7 @@ import ( "next.orly.dev/pkg/encoders/kind" "next.orly.dev/pkg/interfaces/publisher" "next.orly.dev/pkg/interfaces/typer" + "next.orly.dev/pkg/policy" "next.orly.dev/pkg/protocol/publish" "next.orly.dev/pkg/utils" ) @@ -183,36 +184,12 @@ func (p *P) Deliver(ev *event.E) { // either the event pubkey or appears in any 'p' tag of the event. // Only check authentication if AuthRequired is true (ACL is active) if kind.IsPrivileged(ev.Kind) && d.sub.AuthRequired { - if len(d.sub.AuthedPubkey) == 0 { - // Not authenticated - cannot see privileged events - log.D.F( - "subscription delivery DENIED for privileged event %s to %s (not authenticated)", - hex.Enc(ev.ID), d.sub.remote, - ) - continue - } - pk := d.sub.AuthedPubkey - allowed := false - // Direct author match - if utils.FastEqual(ev.Pubkey, pk) { - allowed = true - } else if ev.Tags != nil { - for _, pTag := range ev.Tags.GetAll([]byte("p")) { - // pTag.Value() returns []byte hex string; decode to bytes - dec, derr := hex.Dec(string(pTag.Value())) - if derr != nil { - continue - } - if utils.FastEqual(dec, pk) { - allowed = true - break - } - } - } - if !allowed { + + // Use centralized IsPartyInvolved function for consistent privilege checking + if !policy.IsPartyInvolved(ev, pk) { log.D.F( - "subscription delivery DENIED for privileged event %s to %s (auth mismatch)", + "subscription delivery DENIED for privileged event %s to %s (not authenticated or not a party involved)", hex.Enc(ev.ID), d.sub.remote, ) // Skip delivery for this subscriber diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 2d802e2..984a357 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -271,6 +271,43 @@ func New(policyJSON []byte) (p *P, err error) { return } +// IsPartyInvolved checks if the given pubkey is a party involved in the event. +// A party is involved if they are either: +// 1. The author of the event (ev.Pubkey == userPubkey) +// 2. Mentioned in a p-tag of the event +// +// Both ev.Pubkey and userPubkey must be binary ([]byte), not hex-encoded. +// P-tags are assumed to contain hex-encoded pubkeys that will be decoded. +// +// This is the single source of truth for "parties_involved" / "privileged" checks. +func IsPartyInvolved(ev *event.E, userPubkey []byte) bool { + // Must be authenticated + if len(userPubkey) == 0 { + return false + } + + // Check if user is the author + if bytes.Equal(ev.Pubkey, userPubkey) { + return true + } + + // Check if user is in p tags + pTags := ev.Tags.GetAll([]byte("p")) + for _, pTag := range pTags { + // pTag.Value() returns hex-encoded string; decode to bytes for comparison + pt, err := hex.Dec(string(pTag.Value())) + if err != nil { + // Skip malformed tags + continue + } + if bytes.Equal(pt, userPubkey) { + return true + } + } + + return false +} + // getDefaultPolicyAction returns true if the default policy is "allow", false if "deny" func (p *P) getDefaultPolicyAction() (allowed bool) { switch p.DefaultPolicy { @@ -999,6 +1036,7 @@ func (p *P) checkRulePolicy( } else if access == "read" { // For read access, check the logged-in user's pubkey (who is trying to READ), // not the event author's pubkey + // Prefer binary cache for performance (3x faster than hex) // Fall back to hex comparison if cache not populated (for backwards compatibility with tests) if len(rule.readAllowBin) > 0 { @@ -1095,30 +1133,12 @@ func (p *P) checkRulePolicy( } } - // Check privileged events + // Check privileged events using centralized function if rule.Privileged { - if len(loggedInPubkey) == 0 { - return false, nil // Must be authenticated - } - // Check if event is authored by logged in user or contains logged in user in p tags - if !bytes.Equal(ev.Pubkey, loggedInPubkey) { - // Check p tags - pTags := ev.Tags.GetAll([]byte("p")) - found := false - for _, pTag := range pTags { - // pTag.Value() returns hex-encoded string; decode to bytes - pt, err := hex.Dec(string(pTag.Value())) - if err != nil { - continue - } - if bytes.Equal(pt, loggedInPubkey) { - found = true - break - } - } - if !found { - return false, nil - } + // Use the centralized IsPartyInvolved function to check + // This ensures consistent hex/binary handling across all privilege checks + if !IsPartyInvolved(ev, loggedInPubkey) { + return false, nil } }