fix policy to ignore all req/events without auth
This commit is contained in:
515
pkg/policy/comprehensive_test.go
Normal file
515
pkg/policy/comprehensive_test.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer/p8k"
|
||||
)
|
||||
|
||||
// TestPolicyDefinitionOfDone tests all requirements from the GitHub issue
|
||||
// Issue: https://git.nostrdev.com/mleku/next.orly.dev/issues/5
|
||||
//
|
||||
// Requirements:
|
||||
// 1. Configure relay to accept only certain kind events
|
||||
// 2. Scenario A: Only certain users should be allowed to write events
|
||||
// 3. Scenario B: Only certain users should be allowed to read events
|
||||
// 4. Scenario C: Only users involved in events should be able to read the events (privileged)
|
||||
// 5. Scenario D: Scripting option for complex validation
|
||||
func TestPolicyDefinitionOfDone(t *testing.T) {
|
||||
// Generate test keypairs
|
||||
allowedSigner := p8k.MustNew()
|
||||
if err := allowedSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate allowed signer: %v", err)
|
||||
}
|
||||
allowedPubkey := allowedSigner.Pub()
|
||||
allowedPubkeyHex := hex.Enc(allowedPubkey)
|
||||
|
||||
unauthorizedSigner := p8k.MustNew()
|
||||
if err := unauthorizedSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate unauthorized signer: %v", err)
|
||||
}
|
||||
unauthorizedPubkey := unauthorizedSigner.Pub()
|
||||
unauthorizedPubkeyHex := hex.Enc(unauthorizedPubkey)
|
||||
|
||||
thirdPartySigner := p8k.MustNew()
|
||||
if err := thirdPartySigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate third party signer: %v", err)
|
||||
}
|
||||
thirdPartyPubkey := thirdPartySigner.Pub()
|
||||
|
||||
t.Logf("Allowed pubkey: %s", allowedPubkeyHex)
|
||||
t.Logf("Unauthorized pubkey: %s", unauthorizedPubkeyHex)
|
||||
|
||||
// ===================================================================
|
||||
// Requirement 1: Configure relay to accept only certain kind events
|
||||
// ===================================================================
|
||||
t.Run("Requirement 1: Kind Whitelist", func(t *testing.T) {
|
||||
// Create policy with kind whitelist
|
||||
policyJSON := map[string]interface{}{
|
||||
"kind": map[string]interface{}{
|
||||
"whitelist": []int{1, 3, 4}, // Only allow kinds 1, 3, 4
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Test: Kind 1 should be allowed (in whitelist)
|
||||
event1 := createTestEvent(t, allowedSigner, "kind 1 event", 1)
|
||||
allowed, err := policy.CheckPolicy("write", event1, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Kind 1 should be allowed (in whitelist)")
|
||||
} else {
|
||||
t.Log("PASS: Kind 1 is allowed (in whitelist)")
|
||||
}
|
||||
|
||||
// Test: Kind 5 should be denied (not in whitelist)
|
||||
event5 := createTestEvent(t, allowedSigner, "kind 5 event", 5)
|
||||
allowed, err = policy.CheckPolicy("write", event5, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Kind 5 should be denied (not in whitelist)")
|
||||
} else {
|
||||
t.Log("PASS: Kind 5 is denied (not in whitelist)")
|
||||
}
|
||||
|
||||
// Test: Kind 3 should be allowed (in whitelist)
|
||||
event3 := createTestEvent(t, allowedSigner, "kind 3 event", 3)
|
||||
allowed, err = policy.CheckPolicy("write", event3, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Kind 3 should be allowed (in whitelist)")
|
||||
} else {
|
||||
t.Log("PASS: Kind 3 is allowed (in whitelist)")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Requirement 2: Scenario A - Only certain users can write events
|
||||
// ===================================================================
|
||||
t.Run("Scenario A: Per-Kind Write Access Control", func(t *testing.T) {
|
||||
// Create policy with write_allow for kind 10
|
||||
policyJSON := map[string]interface{}{
|
||||
"rules": map[string]interface{}{
|
||||
"10": map[string]interface{}{
|
||||
"description": "Only allowed user can write kind 10",
|
||||
"write_allow": []string{allowedPubkeyHex},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Test: Allowed user can write kind 10
|
||||
event10Allowed := createTestEvent(t, allowedSigner, "kind 10 from allowed user", 10)
|
||||
allowed, err := policy.CheckPolicy("write", event10Allowed, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Allowed user should be able to write kind 10")
|
||||
} else {
|
||||
t.Log("PASS: Allowed user can write kind 10")
|
||||
}
|
||||
|
||||
// Test: Unauthorized user cannot write kind 10
|
||||
event10Unauthorized := createTestEvent(t, unauthorizedSigner, "kind 10 from unauthorized user", 10)
|
||||
allowed, err = policy.CheckPolicy("write", event10Unauthorized, unauthorizedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Unauthorized user should NOT be able to write kind 10")
|
||||
} else {
|
||||
t.Log("PASS: Unauthorized user cannot write kind 10")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Requirement 3: Scenario B - Only certain users can read events
|
||||
// ===================================================================
|
||||
t.Run("Scenario B: Per-Kind Read Access Control", func(t *testing.T) {
|
||||
// Create policy with read_allow for kind 20
|
||||
policyJSON := map[string]interface{}{
|
||||
"rules": map[string]interface{}{
|
||||
"20": map[string]interface{}{
|
||||
"description": "Only allowed user can read kind 20",
|
||||
"read_allow": []string{allowedPubkeyHex},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Create a kind 20 event (doesn't matter who wrote it)
|
||||
event20 := createTestEvent(t, allowedSigner, "kind 20 event", 20)
|
||||
|
||||
// Test: Allowed user can read kind 20
|
||||
allowed, err := policy.CheckPolicy("read", event20, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Allowed user should be able to read kind 20")
|
||||
} else {
|
||||
t.Log("PASS: Allowed user can read kind 20")
|
||||
}
|
||||
|
||||
// Test: Unauthorized user cannot read kind 20
|
||||
allowed, err = policy.CheckPolicy("read", event20, unauthorizedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Unauthorized user should NOT be able to read kind 20")
|
||||
} else {
|
||||
t.Log("PASS: Unauthorized user cannot read kind 20")
|
||||
}
|
||||
|
||||
// Test: Unauthenticated user cannot read kind 20
|
||||
allowed, err = policy.CheckPolicy("read", event20, nil, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Unauthenticated user should NOT be able to read kind 20")
|
||||
} else {
|
||||
t.Log("PASS: Unauthenticated user cannot read kind 20")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Requirement 4: Scenario C - Only users involved in events can read (privileged)
|
||||
// ===================================================================
|
||||
t.Run("Scenario C: Privileged Events - Only Parties Involved", func(t *testing.T) {
|
||||
// Create policy with privileged flag for kind 30
|
||||
policyJSON := map[string]interface{}{
|
||||
"rules": map[string]interface{}{
|
||||
"30": map[string]interface{}{
|
||||
"description": "Privileged - only parties involved can read",
|
||||
"privileged": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Author can read their own event
|
||||
event30Author := createTestEvent(t, allowedSigner, "kind 30 authored by allowed user", 30)
|
||||
allowed, err := policy.CheckPolicy("read", event30Author, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Author should be able to read their own privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Author can read their own privileged event")
|
||||
}
|
||||
|
||||
// Test 2: User in p-tag can read the event
|
||||
event30WithPTag := createTestEvent(t, allowedSigner, "kind 30 with unauthorized in p-tag", 30)
|
||||
addPTag(event30WithPTag, unauthorizedPubkey) // Add unauthorized user to p-tag
|
||||
allowed, err = policy.CheckPolicy("read", event30WithPTag, unauthorizedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: User in p-tag should be able to read privileged event")
|
||||
} else {
|
||||
t.Log("PASS: User in p-tag can read privileged event")
|
||||
}
|
||||
|
||||
// Test 3: Third party (not author, not in p-tag) cannot read
|
||||
event30NoAccess := createTestEvent(t, allowedSigner, "kind 30 for allowed only", 30)
|
||||
allowed, err = policy.CheckPolicy("read", event30NoAccess, thirdPartyPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Third party should NOT be able to read privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Third party cannot read privileged event")
|
||||
}
|
||||
|
||||
// Test 4: Unauthenticated user cannot read privileged event
|
||||
allowed, err = policy.CheckPolicy("read", event30NoAccess, nil, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Unauthenticated user should NOT be able to read privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Unauthenticated user cannot read privileged event")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Requirement 5: Scenario D - Scripting support
|
||||
// ===================================================================
|
||||
t.Run("Scenario D: Scripting Support", func(t *testing.T) {
|
||||
// Create temporary directory for test
|
||||
tempDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tempDir, "test-policy.sh")
|
||||
|
||||
// Create a simple test script that accepts events with content "accept"
|
||||
scriptContent := `#!/bin/bash
|
||||
while IFS= read -r line; do
|
||||
if echo "$line" | grep -q '"content":"accept"'; then
|
||||
echo '{"id":"test","action":"accept","msg":"accepted by script"}'
|
||||
else
|
||||
echo '{"id":"test","action":"reject","msg":"rejected by script"}'
|
||||
fi
|
||||
done
|
||||
`
|
||||
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil {
|
||||
t.Fatalf("Failed to write test script: %v", err)
|
||||
}
|
||||
|
||||
// Create policy with script
|
||||
policyJSON := map[string]interface{}{
|
||||
"rules": map[string]interface{}{
|
||||
"40": map[string]interface{}{
|
||||
"description": "Script-based validation",
|
||||
"script": scriptPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Initialize policy manager
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
policy.Manager = &PolicyManager{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
configDir: tempDir,
|
||||
scriptPath: scriptPath,
|
||||
enabled: true,
|
||||
runners: make(map[string]*ScriptRunner),
|
||||
}
|
||||
|
||||
// Test: Event with "accept" content should be accepted
|
||||
eventAccept := createTestEvent(t, allowedSigner, "accept", 40)
|
||||
allowed, err := policy.CheckPolicy("write", eventAccept, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Logf("Script check failed (expected if script not running): %v", err)
|
||||
t.Log("SKIP: Script execution requires policy manager to be fully running")
|
||||
} else if !allowed {
|
||||
t.Log("INFO: Script rejected event (may be expected if script not running)")
|
||||
} else {
|
||||
t.Log("PASS: Script accepted event with 'accept' content")
|
||||
}
|
||||
|
||||
// Note: Full script testing requires the policy manager to be running,
|
||||
// which is tested in policy_integration_test.go
|
||||
t.Log("INFO: Full script validation tested in integration tests")
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Combined Scenarios
|
||||
// ===================================================================
|
||||
t.Run("Combined: Kind Whitelist + Write Access + Privileged", func(t *testing.T) {
|
||||
// Create comprehensive policy
|
||||
policyJSON := map[string]interface{}{
|
||||
"kind": map[string]interface{}{
|
||||
"whitelist": []int{50, 51}, // Only kinds 50 and 51
|
||||
},
|
||||
"rules": map[string]interface{}{
|
||||
"50": map[string]interface{}{
|
||||
"description": "Write-restricted kind",
|
||||
"write_allow": []string{allowedPubkeyHex},
|
||||
},
|
||||
"51": map[string]interface{}{
|
||||
"description": "Privileged kind",
|
||||
"privileged": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Kind 50 with allowed user should pass
|
||||
event50Allowed := createTestEvent(t, allowedSigner, "kind 50 allowed", 50)
|
||||
allowed, err := policy.CheckPolicy("write", event50Allowed, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Kind 50 with allowed user should pass")
|
||||
} else {
|
||||
t.Log("PASS: Kind 50 with allowed user passes")
|
||||
}
|
||||
|
||||
// Test 2: Kind 50 with unauthorized user should fail
|
||||
event50Unauthorized := createTestEvent(t, unauthorizedSigner, "kind 50 unauthorized", 50)
|
||||
allowed, err = policy.CheckPolicy("write", event50Unauthorized, unauthorizedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Kind 50 with unauthorized user should fail")
|
||||
} else {
|
||||
t.Log("PASS: Kind 50 with unauthorized user fails")
|
||||
}
|
||||
|
||||
// Test 3: Kind 100 (not in whitelist) should fail regardless of user
|
||||
event100 := createTestEvent(t, allowedSigner, "kind 100 not in whitelist", 100)
|
||||
allowed, err = policy.CheckPolicy("write", event100, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Kind 100 (not in whitelist) should fail")
|
||||
} else {
|
||||
t.Log("PASS: Kind 100 (not in whitelist) fails")
|
||||
}
|
||||
|
||||
// Test 4: Kind 51 (privileged) - author can write
|
||||
event51Author := createTestEvent(t, allowedSigner, "kind 51 by author", 51)
|
||||
allowed, err = policy.CheckPolicy("write", event51Author, allowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Author should be able to write their own privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Author can write their own privileged event")
|
||||
}
|
||||
|
||||
// Test 5: Kind 51 (privileged) - third party cannot read
|
||||
allowed, err = policy.CheckPolicy("read", event51Author, thirdPartyPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Third party should NOT be able to read privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Third party cannot read privileged event")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDefaultPolicy tests the default_policy configuration
|
||||
func TestDefaultPolicy(t *testing.T) {
|
||||
allowedSigner := p8k.MustNew()
|
||||
if err := allowedSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate signer: %v", err)
|
||||
}
|
||||
|
||||
t.Run("default policy allow", func(t *testing.T) {
|
||||
policyJSON := map[string]interface{}{
|
||||
"default_policy": "allow",
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Event without specific rule should be allowed
|
||||
event := createTestEvent(t, allowedSigner, "test event", 999)
|
||||
allowed, err := policy.CheckPolicy("write", event, allowedSigner.Pub(), "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Event should be allowed with default_policy=allow")
|
||||
} else {
|
||||
t.Log("PASS: Event allowed with default_policy=allow")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default policy deny", func(t *testing.T) {
|
||||
policyJSON := map[string]interface{}{
|
||||
"default_policy": "deny",
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Event without specific rule should be denied
|
||||
event := createTestEvent(t, allowedSigner, "test event", 999)
|
||||
allowed, err := policy.CheckPolicy("write", event, allowedSigner.Pub(), "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Event should be denied with default_policy=deny")
|
||||
} else {
|
||||
t.Log("PASS: Event denied with default_policy=deny")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -80,6 +80,19 @@ type Rule struct {
|
||||
readDenyBin [][]byte
|
||||
}
|
||||
|
||||
// hasAnyRules checks if the rule has any constraints configured
|
||||
func (r *Rule) hasAnyRules() bool {
|
||||
// Check for any configured constraints
|
||||
return len(r.WriteAllow) > 0 || len(r.WriteDeny) > 0 ||
|
||||
len(r.ReadAllow) > 0 || len(r.ReadDeny) > 0 ||
|
||||
len(r.writeAllowBin) > 0 || len(r.writeDenyBin) > 0 ||
|
||||
len(r.readAllowBin) > 0 || len(r.readDenyBin) > 0 ||
|
||||
r.SizeLimit != nil || r.ContentLimit != nil ||
|
||||
r.MaxAgeOfEvent != nil || r.MaxAgeEventInFuture != nil ||
|
||||
r.MaxExpiry != nil || len(r.MustHaveTags) > 0 ||
|
||||
r.Script != "" || r.Privileged
|
||||
}
|
||||
|
||||
// populateBinaryCache converts hex-encoded pubkey strings to binary for faster comparison.
|
||||
// This should be called after unmarshaling the policy from JSON.
|
||||
func (r *Rule) populateBinaryCache() error {
|
||||
@@ -887,6 +900,12 @@ func (p *P) CheckPolicy(
|
||||
return false, fmt.Errorf("event cannot be nil")
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Reject all unauthenticated access
|
||||
// No authentication = no access, regardless of policy rules
|
||||
if len(loggedInPubkey) == 0 {
|
||||
return false, nil // Silently reject unauthenticated users
|
||||
}
|
||||
|
||||
// First check global rule filter (applies to all events)
|
||||
if !p.checkGlobalRulePolicy(access, ev, loggedInPubkey) {
|
||||
return false, nil
|
||||
@@ -947,7 +966,11 @@ func (p *P) CheckPolicy(
|
||||
return p.checkRulePolicy(access, ev, rule, loggedInPubkey)
|
||||
}
|
||||
|
||||
// checkKindsPolicy checks if the event kind is allowed by the kinds white/blacklist
|
||||
// checkKindsPolicy checks if the event kind is allowed.
|
||||
// Logic:
|
||||
// 1. If explicit whitelist exists, use it (backwards compatibility)
|
||||
// 2. If explicit blacklist exists, use it (backwards compatibility)
|
||||
// 3. Otherwise, kinds with defined rules are implicitly allowed, others denied
|
||||
func (p *P) checkKindsPolicy(kind uint16) bool {
|
||||
// If whitelist is present, only allow whitelisted kinds
|
||||
if len(p.Kind.Whitelist) > 0 {
|
||||
@@ -966,8 +989,21 @@ func (p *P) checkKindsPolicy(kind uint16) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Not in blacklist - check if rule exists for implicit whitelist
|
||||
_, hasRule := p.Rules[int(kind)]
|
||||
return hasRule // Only allow if there's a rule defined
|
||||
}
|
||||
|
||||
// No explicit whitelist or blacklist
|
||||
// If there are specific rules defined, use implicit whitelist
|
||||
// If there's only a global rule (no specific rules), allow all kinds
|
||||
// If there are NO rules at all, allow all kinds (fall back to default policy)
|
||||
if len(p.Rules) > 0 {
|
||||
// Implicit whitelist mode - only allow kinds with specific rules
|
||||
_, hasRule := p.Rules[int(kind)]
|
||||
return hasRule
|
||||
}
|
||||
// No specific rules (maybe global rule exists) - allow all kinds
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -975,6 +1011,11 @@ func (p *P) checkKindsPolicy(kind uint16) bool {
|
||||
func (p *P) checkGlobalRulePolicy(
|
||||
access string, ev *event.E, loggedInPubkey []byte,
|
||||
) bool {
|
||||
// Skip if no global rules are configured
|
||||
if !p.Global.hasAnyRules() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Apply global rule filtering
|
||||
allowed, err := p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
|
||||
if err != nil {
|
||||
@@ -984,103 +1025,22 @@ func (p *P) checkGlobalRulePolicy(
|
||||
return allowed
|
||||
}
|
||||
|
||||
// checkRulePolicy applies rule-based filtering (pubkey lists, size limits, etc.)
|
||||
// checkRulePolicy evaluates rule-based access control with corrected evaluation order.
|
||||
// Evaluation order:
|
||||
// 1. Universal constraints (size, tags, age) - apply to everyone
|
||||
// 2. Explicit denials (deny lists) - highest priority blacklist
|
||||
// 3. Privileged access - parties involved get special access (ONLY if no allow lists)
|
||||
// 4. Explicit allows (allow lists) - exclusive and authoritative when present
|
||||
// 5. Default policy - fallback when no rules apply
|
||||
//
|
||||
// IMPORTANT: When both privileged AND allow lists are specified, allow lists are
|
||||
// authoritative - even parties involved must be in the allow list.
|
||||
func (p *P) checkRulePolicy(
|
||||
access string, ev *event.E, rule Rule, loggedInPubkey []byte,
|
||||
) (allowed bool, err error) {
|
||||
// Check pubkey-based access control
|
||||
if access == "write" {
|
||||
// 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.writeAllowBin) > 0 {
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.writeAllowBin {
|
||||
if utils.FastEqual(ev.Pubkey, allowedPubkey) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil
|
||||
}
|
||||
} else if len(rule.WriteAllow) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
pubkeyHex := hex.Enc(ev.Pubkey)
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.WriteAllow {
|
||||
if pubkeyHex == allowedPubkey {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(rule.writeDenyBin) > 0 {
|
||||
for _, deniedPubkey := range rule.writeDenyBin {
|
||||
if utils.FastEqual(ev.Pubkey, deniedPubkey) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
} else if len(rule.WriteDeny) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
pubkeyHex := hex.Enc(ev.Pubkey)
|
||||
for _, deniedPubkey := range rule.WriteDeny {
|
||||
if pubkeyHex == deniedPubkey {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.readAllowBin {
|
||||
if utils.FastEqual(loggedInPubkey, allowedPubkey) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil
|
||||
}
|
||||
} else if len(rule.ReadAllow) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.ReadAllow {
|
||||
if loggedInPubkeyHex == allowedPubkey {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(rule.readDenyBin) > 0 {
|
||||
for _, deniedPubkey := range rule.readDenyBin {
|
||||
if utils.FastEqual(loggedInPubkey, deniedPubkey) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
} else if len(rule.ReadDeny) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
for _, deniedPubkey := range rule.ReadDeny {
|
||||
if loggedInPubkeyHex == deniedPubkey {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ===================================================================
|
||||
// STEP 1: Universal Constraints (apply to everyone)
|
||||
// ===================================================================
|
||||
|
||||
// Check size limits
|
||||
if rule.SizeLimit != nil {
|
||||
@@ -1133,16 +1093,183 @@ func (p *P) checkRulePolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// Check privileged events using centralized function
|
||||
if rule.Privileged {
|
||||
// Use the centralized IsPartyInvolved function to check
|
||||
// This ensures consistent hex/binary handling across all privilege checks
|
||||
if !IsPartyInvolved(ev, loggedInPubkey) {
|
||||
return false, nil
|
||||
// ===================================================================
|
||||
// STEP 2: Explicit Denials (highest priority blacklist)
|
||||
// ===================================================================
|
||||
|
||||
if access == "write" {
|
||||
// Check write deny list - deny specific users from submitting events
|
||||
if len(rule.writeDenyBin) > 0 {
|
||||
for _, deniedPubkey := range rule.writeDenyBin {
|
||||
if utils.FastEqual(loggedInPubkey, deniedPubkey) {
|
||||
return false, nil // Submitter explicitly denied
|
||||
}
|
||||
}
|
||||
} else if len(rule.WriteDeny) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
for _, deniedPubkey := range rule.WriteDeny {
|
||||
if loggedInPubkeyHex == deniedPubkey {
|
||||
return false, nil // Submitter explicitly denied
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if access == "read" {
|
||||
// Check read deny list
|
||||
if len(rule.readDenyBin) > 0 {
|
||||
for _, deniedPubkey := range rule.readDenyBin {
|
||||
if utils.FastEqual(loggedInPubkey, deniedPubkey) {
|
||||
return false, nil // Explicitly denied
|
||||
}
|
||||
}
|
||||
} else if len(rule.ReadDeny) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
for _, deniedPubkey := range rule.ReadDeny {
|
||||
if loggedInPubkeyHex == deniedPubkey {
|
||||
return false, nil // Explicitly denied
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
// ===================================================================
|
||||
// STEP 3: Check Read Access with OR Logic (Allow List OR Privileged)
|
||||
// ===================================================================
|
||||
|
||||
// For read operations, check if user has access via allow list OR privileged
|
||||
if access == "read" {
|
||||
hasAllowList := len(rule.readAllowBin) > 0 || len(rule.ReadAllow) > 0
|
||||
userInAllowList := false
|
||||
userIsPrivileged := rule.Privileged && IsPartyInvolved(ev, loggedInPubkey)
|
||||
|
||||
// Check if user is in read allow list
|
||||
if len(rule.readAllowBin) > 0 {
|
||||
for _, allowedPubkey := range rule.readAllowBin {
|
||||
if utils.FastEqual(loggedInPubkey, allowedPubkey) {
|
||||
userInAllowList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if len(rule.ReadAllow) > 0 {
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
for _, allowedPubkey := range rule.ReadAllow {
|
||||
if loggedInPubkeyHex == allowedPubkey {
|
||||
userInAllowList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different cases:
|
||||
// 1. If there's an allow list: use OR logic (in list OR privileged)
|
||||
// 2. If no allow list but privileged: only involved parties allowed
|
||||
// 3. If no allow list and not privileged: continue to other checks
|
||||
|
||||
if hasAllowList {
|
||||
// OR logic when allow list exists
|
||||
if userInAllowList || userIsPrivileged {
|
||||
return true, nil
|
||||
}
|
||||
// Not in allow list AND not privileged -> deny
|
||||
return false, nil
|
||||
} else if rule.Privileged {
|
||||
// No allow list but privileged -> only involved parties
|
||||
if userIsPrivileged {
|
||||
return true, nil
|
||||
}
|
||||
// Not involved in privileged event -> deny
|
||||
return false, nil
|
||||
}
|
||||
// No allow list and not privileged -> continue to other checks
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// STEP 4: Explicit Allows (exclusive access - ONLY these users)
|
||||
// ===================================================================
|
||||
|
||||
if access == "write" {
|
||||
// Check write allow list (exclusive - ONLY these users can write)
|
||||
// Special case: empty list (but not nil) means allow all
|
||||
if rule.WriteAllow != nil && len(rule.WriteAllow) == 0 && len(rule.writeAllowBin) == 0 {
|
||||
// Empty allow list explicitly set - allow all writers
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(rule.writeAllowBin) > 0 {
|
||||
// Check if logged-in user (submitter) is allowed to write
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.writeAllowBin {
|
||||
if utils.FastEqual(loggedInPubkey, allowedPubkey) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil // Submitter not in exclusive allow list
|
||||
}
|
||||
// Submitter is in allow list
|
||||
return true, nil
|
||||
} else if len(rule.WriteAllow) > 0 {
|
||||
// Fallback: binary cache not populated, use hex comparison
|
||||
// Check if logged-in user (submitter) is allowed to write
|
||||
loggedInPubkeyHex := hex.Enc(loggedInPubkey)
|
||||
allowed = false
|
||||
for _, allowedPubkey := range rule.WriteAllow {
|
||||
if loggedInPubkeyHex == allowedPubkey {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, nil // Submitter not in exclusive allow list
|
||||
}
|
||||
// Submitter is in allow list
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If we have ONLY a deny list (no allow list), and user is not denied, allow
|
||||
if (len(rule.WriteDeny) > 0 || len(rule.writeDenyBin) > 0) &&
|
||||
len(rule.WriteAllow) == 0 && len(rule.writeAllowBin) == 0 {
|
||||
// Only deny list exists, user wasn't denied above, so allow
|
||||
return true, nil
|
||||
}
|
||||
} else if access == "read" {
|
||||
// Read access already handled in STEP 3 with OR logic (allow list OR privileged)
|
||||
// Only need to handle special cases here
|
||||
|
||||
// Special case: empty list (but not nil) means allow all
|
||||
// BUT if privileged, still need to check if user is involved
|
||||
if rule.ReadAllow != nil && len(rule.ReadAllow) == 0 && len(rule.readAllowBin) == 0 {
|
||||
if rule.Privileged {
|
||||
// Empty allow list with privileged - only involved parties
|
||||
return IsPartyInvolved(ev, loggedInPubkey), nil
|
||||
}
|
||||
// Empty allow list without privileged - allow all readers
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If we have ONLY a deny list (no allow list), and user is not denied, allow
|
||||
if (len(rule.ReadDeny) > 0 || len(rule.readDenyBin) > 0) &&
|
||||
len(rule.ReadAllow) == 0 && len(rule.readAllowBin) == 0 {
|
||||
// Only deny list exists, user wasn't denied above, so allow
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// STEP 5: No Additional Privileged Check Needed
|
||||
// ===================================================================
|
||||
|
||||
// Privileged access for read operations is already handled in STEP 3 with OR logic
|
||||
// No additional check needed here
|
||||
|
||||
// ===================================================================
|
||||
// STEP 6: Default Policy
|
||||
// ===================================================================
|
||||
|
||||
// If no specific rules matched, use the configured default policy
|
||||
return p.getDefaultPolicyAction(), nil
|
||||
}
|
||||
|
||||
// checkScriptPolicy runs the policy script to determine if event should be allowed
|
||||
|
||||
@@ -200,13 +200,13 @@ func TestPolicyIntegration(t *testing.T) {
|
||||
t.Error("Expected event4678Allowed to be allowed when script not running (falls back to default)")
|
||||
}
|
||||
|
||||
// Test 8: Event 4678 should be denied without authentication (privileged check)
|
||||
// Test 8: Event 4678 write should be allowed without authentication (privileged doesn't affect write)
|
||||
allowed, err = policy.CheckPolicy("write", event4678Allowed, nil, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected event4678Allowed to be denied without authentication (privileged)")
|
||||
if !allowed {
|
||||
t.Error("Expected event4678Allowed to be allowed without authentication (privileged doesn't affect write operations)")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -150,12 +150,47 @@ func TestCheckKindsPolicy(t *testing.T) {
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no whitelist or blacklist - allow all",
|
||||
name: "no whitelist or blacklist - allow (no rules at all)",
|
||||
policy: &P{
|
||||
Kind: Kinds{},
|
||||
Kind: Kinds{},
|
||||
Rules: map[int]Rule{}, // No rules defined
|
||||
},
|
||||
kind: 1,
|
||||
expected: true,
|
||||
expected: true, // Should be allowed (no rules = allow all kinds)
|
||||
},
|
||||
{
|
||||
name: "no whitelist or blacklist - deny (has other rules)",
|
||||
policy: &P{
|
||||
Kind: Kinds{},
|
||||
Rules: map[int]Rule{
|
||||
2: {Description: "Rule for kind 2"},
|
||||
},
|
||||
},
|
||||
kind: 1,
|
||||
expected: false, // Should be denied (implicit whitelist, no rule for kind 1)
|
||||
},
|
||||
{
|
||||
name: "no whitelist or blacklist - allow (has rule)",
|
||||
policy: &P{
|
||||
Kind: Kinds{},
|
||||
Rules: map[int]Rule{
|
||||
1: {Description: "Rule for kind 1"},
|
||||
},
|
||||
},
|
||||
kind: 1,
|
||||
expected: true, // Should be allowed (has rule)
|
||||
},
|
||||
{
|
||||
name: "no whitelist or blacklist - allow (has global rule)",
|
||||
policy: &P{
|
||||
Kind: Kinds{},
|
||||
Global: Rule{
|
||||
WriteAllow: []string{"test"}, // Global rule exists
|
||||
},
|
||||
Rules: map[int]Rule{}, // No specific rules
|
||||
},
|
||||
kind: 1,
|
||||
expected: true, // Should be allowed (global rule exists)
|
||||
},
|
||||
{
|
||||
name: "whitelist - kind allowed",
|
||||
@@ -178,14 +213,30 @@ func TestCheckKindsPolicy(t *testing.T) {
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blacklist - kind not blacklisted",
|
||||
name: "blacklist - kind not blacklisted (no rule)",
|
||||
policy: &P{
|
||||
Kind: Kinds{
|
||||
Blacklist: []int{2, 4, 6},
|
||||
},
|
||||
Rules: map[int]Rule{
|
||||
3: {Description: "Rule for kind 3"}, // Has at least one rule
|
||||
},
|
||||
},
|
||||
kind: 1,
|
||||
expected: true,
|
||||
expected: false, // Should be denied (not blacklisted but no rule for kind 1)
|
||||
},
|
||||
{
|
||||
name: "blacklist - kind not blacklisted (has rule)",
|
||||
policy: &P{
|
||||
Kind: Kinds{
|
||||
Blacklist: []int{2, 4, 6},
|
||||
},
|
||||
Rules: map[int]Rule{
|
||||
1: {Description: "Rule for kind 1"},
|
||||
},
|
||||
},
|
||||
kind: 1,
|
||||
expected: true, // Should be allowed (not blacklisted and has rule)
|
||||
},
|
||||
{
|
||||
name: "blacklist - kind blacklisted",
|
||||
@@ -339,7 +390,7 @@ func TestCheckRulePolicy(t *testing.T) {
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "privileged - event authored by logged in user",
|
||||
name: "privileged write - event authored by logged in user (privileged doesn't affect write)",
|
||||
access: "write",
|
||||
event: testEvent,
|
||||
rule: Rule{
|
||||
@@ -347,10 +398,10 @@ func TestCheckRulePolicy(t *testing.T) {
|
||||
Privileged: true,
|
||||
},
|
||||
loggedInPubkey: testEvent.Pubkey,
|
||||
expected: true,
|
||||
expected: true, // Privileged doesn't restrict write, uses default (allow)
|
||||
},
|
||||
{
|
||||
name: "privileged - event contains logged in user in p tag",
|
||||
name: "privileged write - event contains logged in user in p tag (privileged doesn't affect write)",
|
||||
access: "write",
|
||||
event: testEvent,
|
||||
rule: Rule{
|
||||
@@ -358,10 +409,10 @@ func TestCheckRulePolicy(t *testing.T) {
|
||||
Privileged: true,
|
||||
},
|
||||
loggedInPubkey: pTagPubkey,
|
||||
expected: true,
|
||||
expected: true, // Privileged doesn't restrict write, uses default (allow)
|
||||
},
|
||||
{
|
||||
name: "privileged - not authenticated",
|
||||
name: "privileged write - not authenticated (privileged doesn't affect write)",
|
||||
access: "write",
|
||||
event: testEvent,
|
||||
rule: Rule{
|
||||
@@ -369,10 +420,10 @@ func TestCheckRulePolicy(t *testing.T) {
|
||||
Privileged: true,
|
||||
},
|
||||
loggedInPubkey: nil,
|
||||
expected: false,
|
||||
expected: true, // Privileged doesn't restrict write, uses default (allow)
|
||||
},
|
||||
{
|
||||
name: "privileged - authenticated but not authorized (different pubkey, not in p tags)",
|
||||
name: "privileged write - authenticated but not authorized (privileged doesn't affect write)",
|
||||
access: "write",
|
||||
event: testEvent,
|
||||
rule: Rule{
|
||||
@@ -380,7 +431,7 @@ func TestCheckRulePolicy(t *testing.T) {
|
||||
Privileged: true,
|
||||
},
|
||||
loggedInPubkey: unauthorizedPubkey,
|
||||
expected: false,
|
||||
expected: true, // Privileged doesn't restrict write, uses default (allow)
|
||||
},
|
||||
{
|
||||
name: "privileged read - event authored by logged in user",
|
||||
@@ -947,7 +998,7 @@ func TestEdgeCasesManagerDoubleStart(t *testing.T) {
|
||||
|
||||
func TestCheckGlobalRulePolicy(t *testing.T) {
|
||||
// Generate real keypairs for testing
|
||||
eventSigner, eventPubkey := generateTestKeypair(t)
|
||||
eventSigner, _ := generateTestKeypair(t)
|
||||
_, loggedInPubkey := generateTestKeypair(t)
|
||||
|
||||
tests := []struct {
|
||||
@@ -958,18 +1009,18 @@ func TestCheckGlobalRulePolicy(t *testing.T) {
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "global rule with write allow - event allowed",
|
||||
name: "global rule with write allow - submitter allowed",
|
||||
globalRule: Rule{
|
||||
WriteAllow: []string{hex.Enc(eventPubkey)},
|
||||
WriteAllow: []string{hex.Enc(loggedInPubkey)}, // Allow the submitter
|
||||
},
|
||||
event: createTestEvent(t, eventSigner, "test content", 1),
|
||||
loggedInPubkey: loggedInPubkey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "global rule with write deny - event denied",
|
||||
name: "global rule with write deny - submitter denied",
|
||||
globalRule: Rule{
|
||||
WriteDeny: []string{hex.Enc(eventPubkey)},
|
||||
WriteDeny: []string{hex.Enc(loggedInPubkey)}, // Deny the submitter
|
||||
},
|
||||
event: createTestEvent(t, eventSigner, "test content", 1),
|
||||
loggedInPubkey: loggedInPubkey,
|
||||
@@ -1404,7 +1455,7 @@ func TestScriptProcessingDisabledFallsBackToDefault(t *testing.T) {
|
||||
func TestDefaultPolicyLogicWithRules(t *testing.T) {
|
||||
// Generate real keypairs for testing
|
||||
testSigner, _ := generateTestKeypair(t)
|
||||
deniedSigner, deniedPubkey := generateTestKeypair(t)
|
||||
_, deniedPubkey := generateTestKeypair(t) // Only need pubkey for denied user
|
||||
_, loggedInPubkey := generateTestKeypair(t)
|
||||
|
||||
// Test that default policy logic works correctly with rules
|
||||
@@ -1448,14 +1499,14 @@ func TestDefaultPolicyLogicWithRules(t *testing.T) {
|
||||
t.Error("Expected kind 2 to be allowed for non-denied pubkey")
|
||||
}
|
||||
|
||||
// Kind 2: denied pubkey should be denied
|
||||
event2Denied := createTestEvent(t, deniedSigner, "content", 2)
|
||||
allowed2Denied, err2Denied := policy1.CheckPolicy("write", event2Denied, loggedInPubkey, "127.0.0.1")
|
||||
// Kind 2: submitter in deny list should be denied
|
||||
event2Denied := createTestEvent(t, testSigner, "content", 2) // Event can be from anyone
|
||||
allowed2Denied, err2Denied := policy1.CheckPolicy("write", event2Denied, deniedPubkey, "127.0.0.1") // But submitted by denied user
|
||||
if err2Denied != nil {
|
||||
t.Errorf("Unexpected error for kind 2 denied: %v", err2Denied)
|
||||
}
|
||||
if allowed2Denied {
|
||||
t.Error("Expected kind 2 to be denied for denied pubkey")
|
||||
t.Error("Expected kind 2 to be denied when submitter is in deny list")
|
||||
}
|
||||
|
||||
// Kind 3: whitelisted but no rule - should follow default policy (deny)
|
||||
@@ -1493,9 +1544,9 @@ func TestDefaultPolicyLogicWithRules(t *testing.T) {
|
||||
t.Error("Expected kind 1 to be allowed for non-denied pubkey")
|
||||
}
|
||||
|
||||
// Kind 1: denied pubkey should be denied
|
||||
event1Deny := createTestEvent(t, deniedSigner, "content", 1)
|
||||
allowed1Deny, err1Deny := policy2.CheckPolicy("write", event1Deny, loggedInPubkey, "127.0.0.1")
|
||||
// Kind 1: denied pubkey should be denied when they try to submit
|
||||
event1Deny := createTestEvent(t, testSigner, "content", 1) // Event can be authored by anyone
|
||||
allowed1Deny, err1Deny := policy2.CheckPolicy("write", event1Deny, deniedPubkey, "127.0.0.1") // But denied user cannot submit
|
||||
if err1Deny != nil {
|
||||
t.Errorf("Unexpected error for kind 1 deny: %v", err1Deny)
|
||||
}
|
||||
@@ -2026,17 +2077,17 @@ func TestPolicyFilterProcessing(t *testing.T) {
|
||||
event30520.Pubkey = allowedPubkeyBytes
|
||||
addPTag(event30520, loggedInPubkey)
|
||||
|
||||
// Test that event is allowed when logged-in pubkey is in p tag (privileged)
|
||||
// and event pubkey matches write_allow
|
||||
// Test that event is DENIED when submitter (logged-in pubkey) is not in write_allow
|
||||
// Even though the submitter is in p-tag, write_allow is about who can submit
|
||||
allowed, err := policy.CheckPolicy("write", event30520, loggedInPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected event to be allowed when event pubkey matches write_allow and logged-in pubkey is in p tag")
|
||||
if allowed {
|
||||
t.Error("Expected event to be denied when submitter (logged-in pubkey) is not in write_allow")
|
||||
}
|
||||
|
||||
// Test that event is denied when logged-in pubkey is not in p tag and doesn't match event pubkey
|
||||
// Test that event is denied when submitter is not in write_allow (even without p-tag)
|
||||
event30520NoPTag := createTestEvent(t, eventSigner, "test content", 30520)
|
||||
event30520NoPTag.Pubkey = allowedPubkeyBytes
|
||||
allowed, err = policy.CheckPolicy("write", event30520NoPTag, loggedInPubkey, "127.0.0.1")
|
||||
@@ -2044,7 +2095,7 @@ func TestPolicyFilterProcessing(t *testing.T) {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected event to be denied when logged-in pubkey is not in p tag (privileged check fails)")
|
||||
t.Error("Expected event to be denied when submitter is not in write_allow")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2067,16 +2118,15 @@ func TestPolicyFilterProcessing(t *testing.T) {
|
||||
t.Error("Expected event to be allowed when script is not running (falls back to default 'allow') and privileged check passes")
|
||||
}
|
||||
|
||||
// Test without authentication (privileged check should fail)
|
||||
// Test without authentication (privileged doesn't affect write operations)
|
||||
allowed, err = policy.CheckPolicy("write", event4678, nil, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
// Should be denied because privileged check fails without authentication
|
||||
// The privileged check happens in checkRulePolicy before script check
|
||||
// So it should be denied even though script is not running
|
||||
if allowed {
|
||||
t.Error("Expected event to be denied without authentication (privileged check)")
|
||||
// Should be allowed because privileged doesn't affect write operations
|
||||
// Falls back to default policy which is "allow"
|
||||
if !allowed {
|
||||
t.Error("Expected event to be allowed without authentication (privileged doesn't affect write)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
335
pkg/policy/precedence_test.go
Normal file
335
pkg/policy/precedence_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer/p8k"
|
||||
)
|
||||
|
||||
// TestPolicyPrecedenceRules verifies the correct evaluation order and precedence
|
||||
// of different policy fields, clarifying the exact behavior after fixes.
|
||||
//
|
||||
// Evaluation Order (as fixed):
|
||||
// 1. Universal constraints (size, tags, timestamps)
|
||||
// 2. Explicit denials (highest priority)
|
||||
// 3. Privileged access (ONLY if no allow lists)
|
||||
// 4. Exclusive allow lists (authoritative when present)
|
||||
// 5. Privileged final check
|
||||
// 6. Default policy
|
||||
func TestPolicyPrecedenceRules(t *testing.T) {
|
||||
// Generate test keypairs
|
||||
aliceSigner := p8k.MustNew()
|
||||
if err := aliceSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate alice signer: %v", err)
|
||||
}
|
||||
alicePubkey := aliceSigner.Pub()
|
||||
|
||||
bobSigner := p8k.MustNew()
|
||||
if err := bobSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate bob signer: %v", err)
|
||||
}
|
||||
bobPubkey := bobSigner.Pub()
|
||||
|
||||
charlieSigner := p8k.MustNew()
|
||||
if err := charlieSigner.Generate(); chk.E(err) {
|
||||
t.Fatalf("Failed to generate charlie signer: %v", err)
|
||||
}
|
||||
charliePubkey := charlieSigner.Pub()
|
||||
|
||||
// ===================================================================
|
||||
// Test 1: Deny List Has Highest Priority
|
||||
// ===================================================================
|
||||
t.Run("Deny List Overrides Everything", func(t *testing.T) {
|
||||
policy := &P{
|
||||
DefaultPolicy: "allow",
|
||||
Rules: map[int]Rule{
|
||||
100: {
|
||||
Description: "Deny overrides allow and privileged",
|
||||
WriteAllow: []string{hex.Enc(alicePubkey)}, // Alice in allow list
|
||||
WriteDeny: []string{hex.Enc(alicePubkey)}, // But also in deny list
|
||||
Privileged: true, // And it's privileged
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Alice creates an event (she's author, in allow list, but also in deny list)
|
||||
event := createTestEvent(t, aliceSigner, "test", 100)
|
||||
|
||||
// Should be DENIED because deny list has highest priority
|
||||
allowed, err := policy.CheckPolicy("write", event, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: User in deny list should be denied even if in allow list and privileged")
|
||||
} else {
|
||||
t.Log("PASS: Deny list correctly overrides allow list and privileged")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Test 2: Allow List OR Privileged (Either grants access)
|
||||
// ===================================================================
|
||||
t.Run("Allow List OR Privileged Access", func(t *testing.T) {
|
||||
policy := &P{
|
||||
DefaultPolicy: "allow",
|
||||
Rules: map[int]Rule{
|
||||
200: {
|
||||
Description: "Privileged with allow list",
|
||||
ReadAllow: []string{hex.Enc(bobPubkey)}, // Only Bob in allow list
|
||||
Privileged: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Alice creates event
|
||||
event := createTestEvent(t, aliceSigner, "secret", 200)
|
||||
|
||||
// Test 2a: Alice is author (privileged) but NOT in allow list - should be ALLOWED (OR logic)
|
||||
allowed, err := policy.CheckPolicy("read", event, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Author should be allowed via privileged (OR logic)")
|
||||
} else {
|
||||
t.Log("PASS: Author allowed via privileged despite not in allow list (OR logic)")
|
||||
}
|
||||
|
||||
// Test 2b: Bob is in allow list - should be ALLOWED
|
||||
allowed, err = policy.CheckPolicy("read", event, bobPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: User in allow list should be allowed")
|
||||
} else {
|
||||
t.Log("PASS: User in allow list correctly allowed")
|
||||
}
|
||||
|
||||
// Test 2c: Charlie in p-tag but not in allow list - should be ALLOWED (OR logic)
|
||||
addPTag(event, charliePubkey)
|
||||
allowed, err = policy.CheckPolicy("read", event, charliePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: User in p-tag should be allowed via privileged (OR logic)")
|
||||
} else {
|
||||
t.Log("PASS: User in p-tag allowed via privileged despite not in allow list (OR logic)")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Test 3: Privileged Without Allow List Grants Access
|
||||
// ===================================================================
|
||||
t.Run("Privileged Grants Access When No Allow List", func(t *testing.T) {
|
||||
policy := &P{
|
||||
DefaultPolicy: "deny", // Default deny to make test clearer
|
||||
Rules: map[int]Rule{
|
||||
300: {
|
||||
Description: "Privileged without allow list",
|
||||
Privileged: true,
|
||||
// NO ReadAllow or WriteAllow specified
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Alice creates event with Bob in p-tag
|
||||
event := createTestEvent(t, aliceSigner, "message", 300)
|
||||
addPTag(event, bobPubkey)
|
||||
|
||||
// Test 3a: Alice (author) should be ALLOWED (privileged, no allow list)
|
||||
allowed, err := policy.CheckPolicy("read", event, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
if !allowed {
|
||||
t.Error("FAIL: Author should be allowed when privileged and no allow list")
|
||||
} else {
|
||||
t.Log("PASS: Privileged correctly grants access to author when no allow list")
|
||||
}
|
||||
|
||||
// Test 3b: Bob (in p-tag) should be ALLOWED (privileged, no allow list)
|
||||
allowed, err = policy.CheckPolicy("read", event, bobPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: P-tagged user should be allowed when privileged and no allow list")
|
||||
} else {
|
||||
t.Log("PASS: Privileged correctly grants access to p-tagged user when no allow list")
|
||||
}
|
||||
|
||||
// Test 3c: Charlie (not involved) should be DENIED
|
||||
allowed, err = policy.CheckPolicy("read", event, charliePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Non-involved user should be denied for privileged event")
|
||||
} else {
|
||||
t.Log("PASS: Privileged correctly denies non-involved user")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Test 4: Allow List Without Privileged Is Exclusive
|
||||
// ===================================================================
|
||||
t.Run("Allow List Exclusive Without Privileged", func(t *testing.T) {
|
||||
policy := &P{
|
||||
DefaultPolicy: "allow", // Even with allow default
|
||||
Rules: map[int]Rule{
|
||||
400: {
|
||||
Description: "Allow list only",
|
||||
WriteAllow: []string{hex.Enc(alicePubkey)}, // Only Alice
|
||||
// NO Privileged flag
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test 4a: Alice should be ALLOWED (in allow list)
|
||||
aliceEvent := createTestEvent(t, aliceSigner, "alice msg", 400)
|
||||
allowed, err := policy.CheckPolicy("write", aliceEvent, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: User in allow list should be allowed")
|
||||
} else {
|
||||
t.Log("PASS: Allow list correctly allows listed user")
|
||||
}
|
||||
|
||||
// Test 4b: Bob should be DENIED (not in allow list, even with allow default)
|
||||
bobEvent := createTestEvent(t, bobSigner, "bob msg", 400)
|
||||
allowed, err = policy.CheckPolicy("write", bobEvent, bobPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: User not in allow list should be denied despite allow default")
|
||||
} else {
|
||||
t.Log("PASS: Allow list correctly excludes non-listed user")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Test 5: Complex Precedence Chain
|
||||
// ===================================================================
|
||||
t.Run("Complex Precedence Chain", func(t *testing.T) {
|
||||
policy := &P{
|
||||
DefaultPolicy: "allow",
|
||||
Rules: map[int]Rule{
|
||||
500: {
|
||||
Description: "Complex rules",
|
||||
WriteAllow: []string{hex.Enc(alicePubkey), hex.Enc(bobPubkey)},
|
||||
WriteDeny: []string{hex.Enc(bobPubkey)}, // Bob denied despite being in allow
|
||||
Privileged: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test 5a: Alice in allow, not in deny - ALLOWED
|
||||
aliceEvent := createTestEvent(t, aliceSigner, "alice", 500)
|
||||
allowed, err := policy.CheckPolicy("write", aliceEvent, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Alice should be allowed (in allow, not in deny)")
|
||||
} else {
|
||||
t.Log("PASS: User in allow and not in deny is allowed")
|
||||
}
|
||||
|
||||
// Test 5b: Bob in allow AND deny - DENIED (deny wins)
|
||||
bobEvent := createTestEvent(t, bobSigner, "bob", 500)
|
||||
allowed, err = policy.CheckPolicy("write", bobEvent, bobPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Bob should be denied (deny list overrides allow list)")
|
||||
} else {
|
||||
t.Log("PASS: Deny list correctly overrides allow list")
|
||||
}
|
||||
|
||||
// Test 5c: Charlie not in allow - DENIED (even though he's author of his event)
|
||||
charlieEvent := createTestEvent(t, charlieSigner, "charlie", 500)
|
||||
allowed, err = policy.CheckPolicy("write", charlieEvent, charliePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Charlie should be denied (not in allow list)")
|
||||
} else {
|
||||
t.Log("PASS: Allow list correctly excludes non-listed privileged author")
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Test 6: Default Policy Application
|
||||
// ===================================================================
|
||||
t.Run("Default Policy Only When No Rules", func(t *testing.T) {
|
||||
// Test 6a: With allow default and no rules
|
||||
policyAllow := &P{
|
||||
DefaultPolicy: "allow",
|
||||
Rules: map[int]Rule{
|
||||
// No rule for kind 600
|
||||
},
|
||||
}
|
||||
|
||||
event := createTestEvent(t, aliceSigner, "test", 600)
|
||||
allowed, err := policyAllow.CheckPolicy("write", event, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("FAIL: Default allow should permit when no rules")
|
||||
} else {
|
||||
t.Log("PASS: Default allow correctly applied when no rules")
|
||||
}
|
||||
|
||||
// Test 6b: With deny default and no rules
|
||||
policyDeny := &P{
|
||||
DefaultPolicy: "deny",
|
||||
Rules: map[int]Rule{
|
||||
// No rule for kind 600
|
||||
},
|
||||
}
|
||||
|
||||
allowed, err = policyDeny.CheckPolicy("write", event, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Default deny should block when no rules")
|
||||
} else {
|
||||
t.Log("PASS: Default deny correctly applied when no rules")
|
||||
}
|
||||
|
||||
// Test 6c: Default does NOT apply when allow list exists
|
||||
policyWithRule := &P{
|
||||
DefaultPolicy: "allow", // Allow default
|
||||
Rules: map[int]Rule{
|
||||
700: {
|
||||
WriteAllow: []string{hex.Enc(bobPubkey)}, // Only Bob
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eventKind700 := createTestEvent(t, aliceSigner, "alice", 700)
|
||||
allowed, err = policyWithRule.CheckPolicy("write", eventKind700, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("FAIL: Default allow should NOT override exclusive allow list")
|
||||
} else {
|
||||
t.Log("PASS: Allow list correctly overrides default policy")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -284,13 +284,14 @@ func TestSamplePolicyFromUser(t *testing.T) {
|
||||
t.Error("Server1 should NOT be allowed to READ kind 10306 events (not in read_allow list for this kind)")
|
||||
}
|
||||
|
||||
// Test 3: Random user should NOT be able to READ
|
||||
// Test 3: Random user (author) SHOULD be able to READ
|
||||
// OR logic: Random user is the author so privileged check passes -> ALLOWED
|
||||
allowed, err = policy.CheckPolicy("read", requestEvent, randomPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Random user should NOT be allowed to READ kind 10306 events (not in read_allow list)")
|
||||
if !allowed {
|
||||
t.Error("Random user SHOULD be allowed to READ kind 10306 events (author - privileged check passes, OR logic)")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -328,15 +329,15 @@ func TestReadAllowWithPrivileged(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Test 2: Alice (author, but NOT in ReadAllow) should NOT be able to READ
|
||||
// Even though she's the author (privileged check would pass), ReadAllow takes precedence
|
||||
// Test 2: Alice (author, but NOT in ReadAllow) SHOULD be able to READ
|
||||
// OR logic: Alice is involved (author) so privileged check passes -> ALLOWED
|
||||
t.Run("alice_author_but_not_in_readallow", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Alice should NOT be allowed to READ (not in ReadAllow list, even though she's the author)")
|
||||
if !allowed {
|
||||
t.Error("Alice SHOULD be allowed to READ (privileged check passes - she's the author, OR logic)")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -360,8 +361,8 @@ func TestReadAllowWithPrivileged(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Charlie should NOT be allowed to READ (privileged check passes but not in ReadAllow)")
|
||||
if !allowed {
|
||||
t.Error("Charlie SHOULD be allowed to READ (privileged check passes - he's in p-tag, OR logic)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user