fixed and unified privilege checks across ACLs
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
This commit is contained in:
@@ -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": []
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,30 +1133,12 @@ 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
|
return false, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user