Some checks failed
Go / build-and-release (push) Has been cancelled
Introduce `read_allow_permissive` and `write_allow_permissive` flags in the global rule to override kind whitelists for read or write operations. These flags allow more flexible policy configurations while maintaining blacklist enforcement and preventing conflicting settings. Updated tests and documentation for clarity.
285 lines
8.7 KiB
Go
285 lines
8.7 KiB
Go
package policy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"lol.mleku.dev/log"
|
|
)
|
|
|
|
// TestBugReproduction_Kind1AllowedWithWhitelist4678 reproduces the reported bug
|
|
// where kind 1 events are being accepted even though only kind 4678 is in the whitelist.
|
|
func TestBugReproduction_Kind1AllowedWithWhitelist4678(t *testing.T) {
|
|
testSigner, testPubkey := generateTestKeypair(t)
|
|
|
|
// Create policy matching the production configuration
|
|
policyJSON := `{
|
|
"kind": { "whitelist": [4678] },
|
|
"rules": {
|
|
"4678": {
|
|
"description": "Zenotp events",
|
|
"script": "policy.sh"
|
|
}
|
|
}
|
|
}`
|
|
|
|
policy, err := New([]byte(policyJSON))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy: %v", err)
|
|
}
|
|
|
|
t.Run("Kind 1 should be REJECTED (not in whitelist)", func(t *testing.T) {
|
|
event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if allowed {
|
|
t.Errorf("BUG REPRODUCED: Kind 1 event was ALLOWED but should be REJECTED (only kind 4678 is whitelisted)")
|
|
t.Logf("Policy whitelist: %v", policy.Kind.Whitelist)
|
|
t.Logf("Policy rules: %v", policy.rules)
|
|
t.Logf("Default policy: %s", policy.DefaultPolicy)
|
|
}
|
|
})
|
|
|
|
t.Run("Kind 4678 should be ALLOWED (in whitelist)", func(t *testing.T) {
|
|
event := createTestEvent(t, testSigner, "Zenotp event", 4678)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Error("Kind 4678 should be ALLOWED (in whitelist)")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestBugReproduction_WithPolicyManager tests with a full policy manager setup
|
|
// to match production environment more closely
|
|
func TestBugReproduction_WithPolicyManager(t *testing.T) {
|
|
testSigner, testPubkey := generateTestKeypair(t)
|
|
|
|
// Create a temporary config directory
|
|
tmpDir := t.TempDir()
|
|
configDir := filepath.Join(tmpDir, "ORLY")
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
t.Fatalf("Failed to create config dir: %v", err)
|
|
}
|
|
|
|
// Write policy configuration matching production
|
|
policyConfig := map[string]interface{}{
|
|
"kind": map[string]interface{}{
|
|
"whitelist": []int{4678},
|
|
},
|
|
"rules": map[string]interface{}{
|
|
"4678": map[string]interface{}{
|
|
"description": "Zenotp events",
|
|
"script": "policy.sh",
|
|
},
|
|
},
|
|
}
|
|
|
|
policyJSON, err := json.MarshalIndent(policyConfig, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal policy JSON: %v", err)
|
|
}
|
|
|
|
policyPath := filepath.Join(configDir, "policy.json")
|
|
if err := os.WriteFile(policyPath, policyJSON, 0644); err != nil {
|
|
t.Fatalf("Failed to write policy file: %v", err)
|
|
}
|
|
|
|
// Create policy with manager (enabled)
|
|
ctx := context.Background()
|
|
policy := NewWithManager(ctx, "ORLY", true)
|
|
|
|
// Load policy from file
|
|
if err := policy.LoadFromFile(policyPath); err != nil {
|
|
t.Fatalf("Failed to load policy from file: %v", err)
|
|
}
|
|
|
|
t.Run("Kind 1 should be REJECTED with PolicyManager", func(t *testing.T) {
|
|
event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if allowed {
|
|
t.Errorf("BUG REPRODUCED: Kind 1 event was ALLOWED but should be REJECTED")
|
|
t.Logf("Policy whitelist: %v", policy.Kind.Whitelist)
|
|
t.Logf("Policy rules: %v", policy.rules)
|
|
t.Logf("Default policy: %s", policy.DefaultPolicy)
|
|
t.Logf("Manager enabled: %v", policy.manager.IsEnabled())
|
|
}
|
|
})
|
|
|
|
t.Run("Kind 4678 should be ALLOWED with PolicyManager", func(t *testing.T) {
|
|
event := createTestEvent(t, testSigner, "Zenotp event", 4678)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Error("Kind 4678 should be ALLOWED (in whitelist)")
|
|
}
|
|
})
|
|
|
|
// Clean up
|
|
if policy.manager != nil {
|
|
policy.manager.Shutdown()
|
|
}
|
|
}
|
|
|
|
// TestBugReproduction_DebugPolicyFlow adds verbose logging to debug the policy flow
|
|
func TestBugReproduction_DebugPolicyFlow(t *testing.T) {
|
|
testSigner, testPubkey := generateTestKeypair(t)
|
|
|
|
policyJSON := `{
|
|
"kind": { "whitelist": [4678] },
|
|
"rules": {
|
|
"4678": {
|
|
"description": "Zenotp events",
|
|
"script": "policy.sh"
|
|
}
|
|
}
|
|
}`
|
|
|
|
policy, err := New([]byte(policyJSON))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy: %v", err)
|
|
}
|
|
|
|
event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
|
|
|
|
t.Logf("=== Policy Configuration ===")
|
|
t.Logf("Whitelist: %v", policy.Kind.Whitelist)
|
|
t.Logf("Blacklist: %v", policy.Kind.Blacklist)
|
|
t.Logf("rules: %v", policy.rules)
|
|
t.Logf("Default policy: %s", policy.DefaultPolicy)
|
|
t.Logf("")
|
|
t.Logf("=== Event Details ===")
|
|
t.Logf("Event kind: %d", event.Kind)
|
|
t.Logf("")
|
|
t.Logf("=== Policy Check Flow ===")
|
|
|
|
// Step 1: Check kinds policy
|
|
kindsAllowed := policy.checkKindsPolicy("write", event.Kind)
|
|
t.Logf("1. checkKindsPolicy(access=write, kind=%d) returned: %v", event.Kind, kindsAllowed)
|
|
|
|
// Full policy check
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
t.Logf("2. CheckPolicy returned: allowed=%v, err=%v", allowed, err)
|
|
|
|
if allowed {
|
|
t.Errorf("BUG REPRODUCED: Kind 1 should be REJECTED but was ALLOWED")
|
|
}
|
|
}
|
|
|
|
// TestBugFix_FailSafeWhenConfigMissing tests the fix for the security bug
|
|
// where missing config would allow all events
|
|
func TestBugFix_FailSafeWhenConfigMissing(t *testing.T) {
|
|
testSigner, testPubkey := generateTestKeypair(t)
|
|
|
|
t.Run("Missing config with enabled policy causes panic", func(t *testing.T) {
|
|
// When policy is enabled but config file is missing, NewWithManager should panic
|
|
// This is a FATAL configuration error that must be fixed before the relay can start
|
|
|
|
defer func() {
|
|
r := recover()
|
|
if r == nil {
|
|
t.Error("Expected panic when policy is enabled but config is missing, but no panic occurred")
|
|
} else {
|
|
// Verify the panic message mentions the config error
|
|
panicMsg := fmt.Sprintf("%v", r)
|
|
if !strings.Contains(panicMsg, "fatal policy configuration error") {
|
|
t.Errorf("Panic message should mention 'fatal policy configuration error', got: %s", panicMsg)
|
|
}
|
|
t.Logf("Correctly panicked with message: %s", panicMsg)
|
|
}
|
|
}()
|
|
|
|
// Simulate NewWithManager behavior by directly testing the panic path
|
|
// Create a policy manager with a non-existent config path
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
configDir := filepath.Join(tmpDir, "ORLY_TEST_NO_CONFIG")
|
|
configPath := filepath.Join(configDir, "policy.json")
|
|
|
|
// Ensure directory exists but file doesn't
|
|
os.MkdirAll(configDir, 0755)
|
|
|
|
manager := &PolicyManager{
|
|
ctx: ctx,
|
|
configDir: configDir,
|
|
scriptPath: filepath.Join(configDir, "policy.sh"),
|
|
enabled: true,
|
|
runners: make(map[string]*ScriptRunner),
|
|
}
|
|
|
|
policy := &P{
|
|
DefaultPolicy: "allow",
|
|
manager: manager,
|
|
}
|
|
|
|
// Try to load from nonexistent file - this should trigger the panic
|
|
if err := policy.LoadFromFile(configPath); err != nil {
|
|
// Simulate what NewWithManager does when LoadFromFile fails
|
|
log.E.F(
|
|
"FATAL: Policy system is ENABLED (ORLY_POLICY_ENABLED=true) but configuration failed to load from %s: %v",
|
|
configPath, err,
|
|
)
|
|
log.E.F("The relay cannot start with an invalid policy configuration.")
|
|
log.E.F("Fix: Either disable the policy system (ORLY_POLICY_ENABLED=false) or ensure %s exists and contains valid JSON", configPath)
|
|
panic(fmt.Sprintf("fatal policy configuration error: %v", err))
|
|
}
|
|
|
|
// Should never reach here
|
|
t.Error("Should have panicked but didn't")
|
|
})
|
|
|
|
t.Run("Empty whitelist respects default_policy=deny", func(t *testing.T) {
|
|
// Create policy with empty whitelist and deny default
|
|
policy := &P{
|
|
DefaultPolicy: "deny",
|
|
Kind: Kinds{
|
|
Whitelist: []int{}, // Empty
|
|
},
|
|
rules: make(map[int]Rule), // No rules
|
|
}
|
|
|
|
event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if allowed {
|
|
t.Error("Kind 1 should be REJECTED with empty whitelist and default_policy=deny")
|
|
}
|
|
})
|
|
|
|
t.Run("Empty whitelist respects default_policy=allow", func(t *testing.T) {
|
|
// Create policy with empty whitelist and allow default
|
|
policy := &P{
|
|
DefaultPolicy: "allow",
|
|
Kind: Kinds{
|
|
Whitelist: []int{}, // Empty
|
|
},
|
|
rules: make(map[int]Rule), // No rules
|
|
}
|
|
|
|
event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
|
|
allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Error("Kind 1 should be ALLOWED with empty whitelist and default_policy=allow")
|
|
}
|
|
})
|
|
}
|