bug_reproduction_test.go raw

   1  package policy
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"fmt"
   7  	"os"
   8  	"path/filepath"
   9  	"strings"
  10  	"testing"
  11  
  12  	"next.orly.dev/pkg/lol/log"
  13  )
  14  
  15  // TestBugReproduction_Kind1AllowedWithWhitelist4678 reproduces the reported bug
  16  // where kind 1 events are being accepted even though only kind 4678 is in the whitelist.
  17  func TestBugReproduction_Kind1AllowedWithWhitelist4678(t *testing.T) {
  18  	testSigner, testPubkey := generateTestKeypair(t)
  19  
  20  	// Create policy matching the production configuration
  21  	policyJSON := `{
  22  		"kind": { "whitelist": [4678] },
  23  		"rules": {
  24  			"4678": {
  25  				"description": "Zenotp events",
  26  				"script": "policy.sh"
  27  			}
  28  		}
  29  	}`
  30  
  31  	policy, err := New([]byte(policyJSON))
  32  	if err != nil {
  33  		t.Fatalf("Failed to create policy: %v", err)
  34  	}
  35  
  36  	t.Run("Kind 1 should be REJECTED (not in whitelist)", func(t *testing.T) {
  37  		event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
  38  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
  39  		if err != nil {
  40  			t.Fatalf("Unexpected error: %v", err)
  41  		}
  42  		if allowed {
  43  			t.Errorf("BUG REPRODUCED: Kind 1 event was ALLOWED but should be REJECTED (only kind 4678 is whitelisted)")
  44  			t.Logf("Policy whitelist: %v", policy.Kind.Whitelist)
  45  			t.Logf("Policy rules: %v", policy.rules)
  46  			t.Logf("Default policy: %s", policy.DefaultPolicy)
  47  		}
  48  	})
  49  
  50  	t.Run("Kind 4678 should be ALLOWED (in whitelist)", func(t *testing.T) {
  51  		event := createTestEvent(t, testSigner, "Zenotp event", 4678)
  52  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
  53  		if err != nil {
  54  			t.Fatalf("Unexpected error: %v", err)
  55  		}
  56  		if !allowed {
  57  			t.Error("Kind 4678 should be ALLOWED (in whitelist)")
  58  		}
  59  	})
  60  }
  61  
  62  // TestBugReproduction_WithPolicyManager tests with a full policy manager setup
  63  // to match production environment more closely
  64  func TestBugReproduction_WithPolicyManager(t *testing.T) {
  65  	testSigner, testPubkey := generateTestKeypair(t)
  66  
  67  	// Create a temporary config directory
  68  	tmpDir := t.TempDir()
  69  	configDir := filepath.Join(tmpDir, "ORLY")
  70  	if err := os.MkdirAll(configDir, 0755); err != nil {
  71  		t.Fatalf("Failed to create config dir: %v", err)
  72  	}
  73  
  74  	// Write policy configuration matching production
  75  	policyConfig := map[string]interface{}{
  76  		"kind": map[string]interface{}{
  77  			"whitelist": []int{4678},
  78  		},
  79  		"rules": map[string]interface{}{
  80  			"4678": map[string]interface{}{
  81  				"description": "Zenotp events",
  82  				"script":      "policy.sh",
  83  			},
  84  		},
  85  	}
  86  
  87  	policyJSON, err := json.MarshalIndent(policyConfig, "", "  ")
  88  	if err != nil {
  89  		t.Fatalf("Failed to marshal policy JSON: %v", err)
  90  	}
  91  
  92  	policyPath := filepath.Join(configDir, "policy.json")
  93  	if err := os.WriteFile(policyPath, policyJSON, 0644); err != nil {
  94  		t.Fatalf("Failed to write policy file: %v", err)
  95  	}
  96  
  97  	// Create policy with manager (enabled)
  98  	ctx := context.Background()
  99  	policy := NewWithManager(ctx, "ORLY", true, "")
 100  
 101  	// Load policy from file
 102  	if err := policy.LoadFromFile(policyPath); err != nil {
 103  		t.Fatalf("Failed to load policy from file: %v", err)
 104  	}
 105  
 106  	t.Run("Kind 1 should be REJECTED with PolicyManager", func(t *testing.T) {
 107  		event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
 108  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
 109  		if err != nil {
 110  			t.Fatalf("Unexpected error: %v", err)
 111  		}
 112  		if allowed {
 113  			t.Errorf("BUG REPRODUCED: Kind 1 event was ALLOWED but should be REJECTED")
 114  			t.Logf("Policy whitelist: %v", policy.Kind.Whitelist)
 115  			t.Logf("Policy rules: %v", policy.rules)
 116  			t.Logf("Default policy: %s", policy.DefaultPolicy)
 117  			t.Logf("Manager enabled: %v", policy.manager.IsEnabled())
 118  		}
 119  	})
 120  
 121  	t.Run("Kind 4678 should be ALLOWED with PolicyManager", func(t *testing.T) {
 122  		event := createTestEvent(t, testSigner, "Zenotp event", 4678)
 123  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
 124  		if err != nil {
 125  			t.Fatalf("Unexpected error: %v", err)
 126  		}
 127  		if !allowed {
 128  			t.Error("Kind 4678 should be ALLOWED (in whitelist)")
 129  		}
 130  	})
 131  
 132  	// Clean up
 133  	if policy.manager != nil {
 134  		policy.manager.Shutdown()
 135  	}
 136  }
 137  
 138  // TestBugReproduction_DebugPolicyFlow adds verbose logging to debug the policy flow
 139  func TestBugReproduction_DebugPolicyFlow(t *testing.T) {
 140  	testSigner, testPubkey := generateTestKeypair(t)
 141  
 142  	policyJSON := `{
 143  		"kind": { "whitelist": [4678] },
 144  		"rules": {
 145  			"4678": {
 146  				"description": "Zenotp events",
 147  				"script": "policy.sh"
 148  			}
 149  		}
 150  	}`
 151  
 152  	policy, err := New([]byte(policyJSON))
 153  	if err != nil {
 154  		t.Fatalf("Failed to create policy: %v", err)
 155  	}
 156  
 157  	event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
 158  
 159  	t.Logf("=== Policy Configuration ===")
 160  	t.Logf("Whitelist: %v", policy.Kind.Whitelist)
 161  	t.Logf("Blacklist: %v", policy.Kind.Blacklist)
 162  	t.Logf("rules: %v", policy.rules)
 163  	t.Logf("Default policy: %s", policy.DefaultPolicy)
 164  	t.Logf("")
 165  	t.Logf("=== Event Details ===")
 166  	t.Logf("Event kind: %d", event.Kind)
 167  	t.Logf("")
 168  	t.Logf("=== Policy Check Flow ===")
 169  
 170  	// Step 1: Check kinds policy
 171  	kindsAllowed := policy.checkKindsPolicy("write", event.Kind)
 172  	t.Logf("1. checkKindsPolicy(access=write, kind=%d) returned: %v", event.Kind, kindsAllowed)
 173  
 174  	// Full policy check
 175  	allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
 176  	t.Logf("2. CheckPolicy returned: allowed=%v, err=%v", allowed, err)
 177  
 178  	if allowed {
 179  		t.Errorf("BUG REPRODUCED: Kind 1 should be REJECTED but was ALLOWED")
 180  	}
 181  }
 182  
 183  // TestBugFix_FailSafeWhenConfigMissing tests the fix for the security bug
 184  // where missing config would allow all events
 185  func TestBugFix_FailSafeWhenConfigMissing(t *testing.T) {
 186  	testSigner, testPubkey := generateTestKeypair(t)
 187  
 188  	t.Run("Missing config with enabled policy causes panic", func(t *testing.T) {
 189  		// When policy is enabled but config file is missing, NewWithManager should panic
 190  		// This is a FATAL configuration error that must be fixed before the relay can start
 191  
 192  		defer func() {
 193  			r := recover()
 194  			if r == nil {
 195  				t.Error("Expected panic when policy is enabled but config is missing, but no panic occurred")
 196  			} else {
 197  				// Verify the panic message mentions the config error
 198  				panicMsg := fmt.Sprintf("%v", r)
 199  				if !strings.Contains(panicMsg, "fatal policy configuration error") {
 200  					t.Errorf("Panic message should mention 'fatal policy configuration error', got: %s", panicMsg)
 201  				}
 202  				t.Logf("Correctly panicked with message: %s", panicMsg)
 203  			}
 204  		}()
 205  
 206  		// Simulate NewWithManager behavior by directly testing the panic path
 207  		// Create a policy manager with a non-existent config path
 208  		ctx := context.Background()
 209  		tmpDir := t.TempDir()
 210  		configDir := filepath.Join(tmpDir, "ORLY_TEST_NO_CONFIG")
 211  		configPath := filepath.Join(configDir, "policy.json")
 212  
 213  		// Ensure directory exists but file doesn't
 214  		os.MkdirAll(configDir, 0755)
 215  
 216  		manager := &PolicyManager{
 217  			ctx:        ctx,
 218  			configDir:  configDir,
 219  			scriptPath: filepath.Join(configDir, "policy.sh"),
 220  			enabled:    true,
 221  			runners:    make(map[string]*ScriptRunner),
 222  		}
 223  
 224  		policy := &P{
 225  			DefaultPolicy: "allow",
 226  			manager:       manager,
 227  		}
 228  
 229  		// Try to load from nonexistent file - this should trigger the panic
 230  		if err := policy.LoadFromFile(configPath); err != nil {
 231  			// Simulate what NewWithManager does when LoadFromFile fails
 232  			log.E.F(
 233  				"FATAL: Policy system is ENABLED (ORLY_POLICY_ENABLED=true) but configuration failed to load from %s: %v",
 234  				configPath, err,
 235  			)
 236  			log.E.F("The relay cannot start with an invalid policy configuration.")
 237  			log.E.F("Fix: Either disable the policy system (ORLY_POLICY_ENABLED=false) or ensure %s exists and contains valid JSON", configPath)
 238  			panic(fmt.Sprintf("fatal policy configuration error: %v", err))
 239  		}
 240  
 241  		// Should never reach here
 242  		t.Error("Should have panicked but didn't")
 243  	})
 244  
 245  	t.Run("Empty whitelist respects default_policy=deny", func(t *testing.T) {
 246  		// Create policy with empty whitelist and deny default
 247  		policy := &P{
 248  			DefaultPolicy: "deny",
 249  			Kind: Kinds{
 250  				Whitelist: []int{}, // Empty
 251  			},
 252  			rules: make(map[int]Rule), // No rules
 253  		}
 254  
 255  		event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
 256  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
 257  		if err != nil {
 258  			t.Fatalf("Unexpected error: %v", err)
 259  		}
 260  		if allowed {
 261  			t.Error("Kind 1 should be REJECTED with empty whitelist and default_policy=deny")
 262  		}
 263  	})
 264  
 265  	t.Run("Empty whitelist respects default_policy=allow", func(t *testing.T) {
 266  		// Create policy with empty whitelist and allow default
 267  		policy := &P{
 268  			DefaultPolicy: "allow",
 269  			Kind: Kinds{
 270  				Whitelist: []int{}, // Empty
 271  			},
 272  			rules: make(map[int]Rule), // No rules
 273  		}
 274  
 275  		event := createTestEvent(t, testSigner, "Hello Nostr!", 1)
 276  		allowed, err := policy.CheckPolicy("write", event, testPubkey, "127.0.0.1")
 277  		if err != nil {
 278  			t.Fatalf("Unexpected error: %v", err)
 279  		}
 280  		if !allowed {
 281  			t.Error("Kind 1 should be ALLOWED with empty whitelist and default_policy=allow")
 282  		}
 283  	})
 284  }
 285