fixed and unified privilege checks across ACLs
Some checks failed
Go / build-and-release (push) Has been cancelled

This commit is contained in:
2025-11-19 13:05:21 +00:00
parent f89f41b8c4
commit a79beee179
4 changed files with 56 additions and 93 deletions

View File

@@ -109,7 +109,8 @@
"Bash(timeout 30 sh:*)", "Bash(timeout 30 sh:*)",
"Bash(timeout 60 go test:*)", "Bash(timeout 60 go test:*)",
"Bash(timeout 120 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": [], "deny": [],
"ask": [] "ask": []

View File

@@ -24,6 +24,7 @@ import (
"next.orly.dev/pkg/encoders/kind" "next.orly.dev/pkg/encoders/kind"
"next.orly.dev/pkg/encoders/reason" "next.orly.dev/pkg/encoders/reason"
"next.orly.dev/pkg/encoders/tag" "next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/policy"
"next.orly.dev/pkg/protocol/nip43" "next.orly.dev/pkg/protocol/nip43"
"next.orly.dev/pkg/utils" "next.orly.dev/pkg/utils"
"next.orly.dev/pkg/utils/normalize" "next.orly.dev/pkg/utils/normalize"
@@ -360,59 +361,23 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
}, },
) )
pk := l.authedPubkey.Load() 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( log.T.C(
func() string { func() string {
return fmt.Sprintf( return fmt.Sprintf(
"privileged event %s denied - not authenticated", "privileged event %s allowed for logged in pubkey %0x",
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",
ev.ID, pk, 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) tmp = append(tmp, ev)
} else { } else {
log.T.C( log.T.C(
func() string { func() string {
return fmt.Sprintf( 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, ev.ID, pk,
) )
}, },

View File

@@ -15,6 +15,7 @@ import (
"next.orly.dev/pkg/encoders/kind" "next.orly.dev/pkg/encoders/kind"
"next.orly.dev/pkg/interfaces/publisher" "next.orly.dev/pkg/interfaces/publisher"
"next.orly.dev/pkg/interfaces/typer" "next.orly.dev/pkg/interfaces/typer"
"next.orly.dev/pkg/policy"
"next.orly.dev/pkg/protocol/publish" "next.orly.dev/pkg/protocol/publish"
"next.orly.dev/pkg/utils" "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. // either the event pubkey or appears in any 'p' tag of the event.
// Only check authentication if AuthRequired is true (ACL is active) // Only check authentication if AuthRequired is true (ACL is active)
if kind.IsPrivileged(ev.Kind) && d.sub.AuthRequired { 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 pk := d.sub.AuthedPubkey
allowed := false
// Direct author match // Use centralized IsPartyInvolved function for consistent privilege checking
if utils.FastEqual(ev.Pubkey, pk) { if !policy.IsPartyInvolved(ev, 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 {
log.D.F( 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, hex.Enc(ev.ID), d.sub.remote,
) )
// Skip delivery for this subscriber // Skip delivery for this subscriber

View File

@@ -271,6 +271,43 @@ func New(policyJSON []byte) (p *P, err error) {
return 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" // getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
func (p *P) getDefaultPolicyAction() (allowed bool) { func (p *P) getDefaultPolicyAction() (allowed bool) {
switch p.DefaultPolicy { switch p.DefaultPolicy {
@@ -999,6 +1036,7 @@ func (p *P) checkRulePolicy(
} else if access == "read" { } else if access == "read" {
// For read access, check the logged-in user's pubkey (who is trying to READ), // For read access, check the logged-in user's pubkey (who is trying to READ),
// not the event author's pubkey // not the event author's pubkey
// Prefer binary cache for performance (3x faster than hex) // Prefer binary cache for performance (3x faster than hex)
// Fall back to hex comparison if cache not populated (for backwards compatibility with tests) // Fall back to hex comparison if cache not populated (for backwards compatibility with tests)
if len(rule.readAllowBin) > 0 { if len(rule.readAllowBin) > 0 {
@@ -1095,32 +1133,14 @@ func (p *P) checkRulePolicy(
} }
} }
// Check privileged events // Check privileged events using centralized function
if rule.Privileged { if rule.Privileged {
if len(loggedInPubkey) == 0 { // Use the centralized IsPartyInvolved function to check
return false, nil // Must be authenticated // This ensures consistent hex/binary handling across all privilege checks
} if !IsPartyInvolved(ev, loggedInPubkey) {
// 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 return false, nil
} }
} }
}
return true, nil return true, nil
} }