handle_policy_config_test.go raw

   1  package app
   2  
   3  import (
   4  	"context"
   5  	"os"
   6  	"path/filepath"
   7  	"sync"
   8  	"testing"
   9  	"time"
  10  
  11  	"github.com/adrg/xdg"
  12  	"next.orly.dev/pkg/nostr/encoders/event"
  13  	"next.orly.dev/pkg/nostr/encoders/hex"
  14  	"next.orly.dev/pkg/nostr/encoders/kind"
  15  	"next.orly.dev/pkg/nostr/encoders/tag"
  16  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  17  	"next.orly.dev/app/config"
  18  	"next.orly.dev/pkg/acl"
  19  	"next.orly.dev/pkg/database"
  20  	"next.orly.dev/pkg/policy"
  21  	"next.orly.dev/pkg/protocol/publish"
  22  )
  23  
  24  // setupPolicyTestListener creates a test listener with policy system enabled
  25  func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *database.D, func()) {
  26  	tempDir, err := os.MkdirTemp("", "policy_handler_test_*")
  27  	if err != nil {
  28  		t.Fatalf("failed to create temp dir: %v", err)
  29  	}
  30  
  31  	// Use a unique app name per test to avoid conflicts
  32  	appName := "test-policy-" + filepath.Base(tempDir)
  33  
  34  	// Create the XDG config directory and default policy file BEFORE creating the policy manager
  35  	configDir := filepath.Join(xdg.ConfigHome, appName)
  36  	if err := os.MkdirAll(configDir, 0755); err != nil {
  37  		os.RemoveAll(tempDir)
  38  		t.Fatalf("failed to create config dir: %v", err)
  39  	}
  40  
  41  	// Create initial policy file with admin if provided
  42  	var initialPolicy []byte
  43  	if policyAdminHex != "" {
  44  		initialPolicy = []byte(`{
  45  			"default_policy": "allow",
  46  			"policy_admins": ["` + policyAdminHex + `"],
  47  			"policy_follow_whitelist_enabled": true
  48  		}`)
  49  	} else {
  50  		initialPolicy = []byte(`{"default_policy": "allow"}`)
  51  	}
  52  	policyPath := filepath.Join(configDir, "policy.json")
  53  	if err := os.WriteFile(policyPath, initialPolicy, 0644); err != nil {
  54  		os.RemoveAll(tempDir)
  55  		os.RemoveAll(configDir)
  56  		t.Fatalf("failed to write policy file: %v", err)
  57  	}
  58  
  59  	ctx, cancel := context.WithCancel(context.Background())
  60  	db, err := database.New(ctx, cancel, tempDir, "info")
  61  	if err != nil {
  62  		os.RemoveAll(tempDir)
  63  		os.RemoveAll(configDir)
  64  		t.Fatalf("failed to open database: %v", err)
  65  	}
  66  
  67  	cfg := &config.C{
  68  		PolicyEnabled: true,
  69  		RelayURL:      "wss://test.relay",
  70  		Listen:        "localhost",
  71  		Port:          3334,
  72  		ACLMode:       "none",
  73  		AppName:       appName,
  74  	}
  75  
  76  	// Create policy manager - now config file exists at XDG path
  77  	policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, "")
  78  
  79  	server := &Server{
  80  		Ctx:             ctx,
  81  		Config:          cfg,
  82  		DB:              db,
  83  		publishers:      publish.New(NewPublisher(ctx)),
  84  		policyManager:   policyManager,
  85  		cfg:             cfg,
  86  		db:              db,
  87  		messagePauseMutex: sync.RWMutex{},
  88  	}
  89  
  90  	// Configure ACL registry
  91  	acl.Registry.SetMode(cfg.ACLMode)
  92  	if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
  93  		db.Close()
  94  		os.RemoveAll(tempDir)
  95  		os.RemoveAll(configDir)
  96  		t.Fatalf("failed to configure ACL: %v", err)
  97  	}
  98  
  99  	listener := &Listener{
 100  		Server:         server,
 101  		ctx:            ctx,
 102  		writeChan:      make(chan publish.WriteRequest, 100),
 103  		writeDone:      make(chan struct{}),
 104  		messageQueue:   make(chan messageRequest, 100),
 105  		processingDone: make(chan struct{}),
 106  		subscriptions:  make(map[string]context.CancelFunc),
 107  	}
 108  
 109  	// Start write worker and message processor
 110  	go listener.writeWorker()
 111  	go listener.messageProcessor()
 112  
 113  	cleanup := func() {
 114  		close(listener.writeChan)
 115  		<-listener.writeDone
 116  		close(listener.messageQueue)
 117  		<-listener.processingDone
 118  		db.Close()
 119  		os.RemoveAll(tempDir)
 120  		os.RemoveAll(configDir)
 121  	}
 122  
 123  	return listener, db, cleanup
 124  }
 125  
 126  // createPolicyConfigEvent creates a kind 12345 policy config event
 127  func createPolicyConfigEvent(t *testing.T, signer *p8k.Signer, policyJSON string) *event.E {
 128  	ev := event.New()
 129  	ev.CreatedAt = time.Now().Unix()
 130  	ev.Kind = kind.PolicyConfig.K
 131  	ev.Content = []byte(policyJSON)
 132  	ev.Tags = tag.NewS()
 133  
 134  	if err := ev.Sign(signer); err != nil {
 135  		t.Fatalf("Failed to sign event: %v", err)
 136  	}
 137  
 138  	return ev
 139  }
 140  
 141  // TestHandlePolicyConfigUpdate_ValidAdmin tests policy update from valid admin
 142  // Policy admins can extend rules but cannot modify protected fields (owners, policy_admins)
 143  func TestHandlePolicyConfigUpdate_ValidAdmin(t *testing.T) {
 144  	// Create admin signer
 145  	adminSigner := p8k.MustNew()
 146  	if err := adminSigner.Generate(); err != nil {
 147  		t.Fatalf("Failed to generate admin keypair: %v", err)
 148  	}
 149  	adminHex := hex.Enc(adminSigner.Pub())
 150  
 151  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 152  	defer cleanup()
 153  
 154  	// Create valid policy update event that ONLY extends, doesn't modify protected fields
 155  	// Note: policy_admins must stay the same (policy admins cannot change this field)
 156  	newPolicyJSON := `{
 157  		"default_policy": "allow",
 158  		"policy_admins": ["` + adminHex + `"],
 159  		"kind": {"whitelist": [1, 3, 7]}
 160  	}`
 161  
 162  	ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
 163  
 164  	// Handle the event
 165  	err := listener.HandlePolicyConfigUpdate(ev)
 166  	if err != nil {
 167  		t.Errorf("Expected success but got error: %v", err)
 168  	}
 169  
 170  	// Verify policy was updated (kind whitelist was extended)
 171  	// Note: default_policy should still be "allow" from original
 172  	if listener.policyManager.DefaultPolicy != "allow" {
 173  		t.Errorf("Policy was not updated correctly, default_policy = %q, expected 'allow'",
 174  			listener.policyManager.DefaultPolicy)
 175  	}
 176  }
 177  
 178  // TestHandlePolicyConfigUpdate_NonAdmin tests policy update rejection from non-admin
 179  func TestHandlePolicyConfigUpdate_NonAdmin(t *testing.T) {
 180  	// Create admin signer
 181  	adminSigner := p8k.MustNew()
 182  	if err := adminSigner.Generate(); err != nil {
 183  		t.Fatalf("Failed to generate admin keypair: %v", err)
 184  	}
 185  	adminHex := hex.Enc(adminSigner.Pub())
 186  
 187  	// Create non-admin signer
 188  	nonAdminSigner := p8k.MustNew()
 189  	if err := nonAdminSigner.Generate(); err != nil {
 190  		t.Fatalf("Failed to generate non-admin keypair: %v", err)
 191  	}
 192  
 193  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 194  	defer cleanup()
 195  
 196  	// Create policy update event from non-admin
 197  	newPolicyJSON := `{"default_policy": "deny"}`
 198  	ev := createPolicyConfigEvent(t, nonAdminSigner, newPolicyJSON)
 199  
 200  	// Handle the event - should be rejected
 201  	err := listener.HandlePolicyConfigUpdate(ev)
 202  	if err == nil {
 203  		t.Error("Expected error for non-admin update but got none")
 204  	}
 205  
 206  	// Verify policy was NOT updated
 207  	if listener.policyManager.DefaultPolicy != "allow" {
 208  		t.Error("Policy should not have been updated by non-admin")
 209  	}
 210  }
 211  
 212  // TestHandlePolicyConfigUpdate_InvalidJSON tests rejection of invalid JSON
 213  func TestHandlePolicyConfigUpdate_InvalidJSON(t *testing.T) {
 214  	adminSigner := p8k.MustNew()
 215  	if err := adminSigner.Generate(); err != nil {
 216  		t.Fatalf("Failed to generate admin keypair: %v", err)
 217  	}
 218  	adminHex := hex.Enc(adminSigner.Pub())
 219  
 220  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 221  	defer cleanup()
 222  
 223  	// Create event with invalid JSON
 224  	ev := createPolicyConfigEvent(t, adminSigner, `{"invalid json`)
 225  
 226  	err := listener.HandlePolicyConfigUpdate(ev)
 227  	if err == nil {
 228  		t.Error("Expected error for invalid JSON but got none")
 229  	}
 230  
 231  	// Policy should remain unchanged
 232  	if listener.policyManager.DefaultPolicy != "allow" {
 233  		t.Error("Policy should not have been updated with invalid JSON")
 234  	}
 235  }
 236  
 237  // TestHandlePolicyConfigUpdate_InvalidPubkey tests rejection of invalid admin pubkeys
 238  func TestHandlePolicyConfigUpdate_InvalidPubkey(t *testing.T) {
 239  	adminSigner := p8k.MustNew()
 240  	if err := adminSigner.Generate(); err != nil {
 241  		t.Fatalf("Failed to generate admin keypair: %v", err)
 242  	}
 243  	adminHex := hex.Enc(adminSigner.Pub())
 244  
 245  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 246  	defer cleanup()
 247  
 248  	// Try to update with invalid admin pubkey
 249  	invalidPolicyJSON := `{
 250  		"default_policy": "deny",
 251  		"policy_admins": ["not-a-valid-pubkey"]
 252  	}`
 253  	ev := createPolicyConfigEvent(t, adminSigner, invalidPolicyJSON)
 254  
 255  	err := listener.HandlePolicyConfigUpdate(ev)
 256  	if err == nil {
 257  		t.Error("Expected error for invalid admin pubkey but got none")
 258  	}
 259  
 260  	// Policy should remain unchanged
 261  	if listener.policyManager.DefaultPolicy != "allow" {
 262  		t.Error("Policy should not have been updated with invalid admin pubkey")
 263  	}
 264  }
 265  
 266  // TestHandlePolicyConfigUpdate_PolicyAdminCannotModifyProtectedFields tests that policy admins
 267  // cannot modify the owners or policy_admins fields (these are protected, owner-only fields)
 268  func TestHandlePolicyConfigUpdate_PolicyAdminCannotModifyProtectedFields(t *testing.T) {
 269  	adminSigner := p8k.MustNew()
 270  	if err := adminSigner.Generate(); err != nil {
 271  		t.Fatalf("Failed to generate admin keypair: %v", err)
 272  	}
 273  	adminHex := hex.Enc(adminSigner.Pub())
 274  
 275  	// Create second admin
 276  	admin2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
 277  
 278  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 279  	defer cleanup()
 280  
 281  	// Try to add second admin (policy_admins is a protected field)
 282  	newPolicyJSON := `{
 283  		"default_policy": "allow",
 284  		"policy_admins": ["` + adminHex + `", "` + admin2Hex + `"]
 285  	}`
 286  	ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
 287  
 288  	// This should FAIL because policy admins cannot modify the policy_admins field
 289  	err := listener.HandlePolicyConfigUpdate(ev)
 290  	if err == nil {
 291  		t.Error("Expected error when policy admin tries to modify policy_admins (protected field)")
 292  	}
 293  
 294  	// Second admin should NOT be in the list since update was rejected
 295  	admin2Bin, _ := hex.Dec(admin2Hex)
 296  	if listener.policyManager.IsPolicyAdmin(admin2Bin) {
 297  		t.Error("Second admin should NOT have been added - policy_admins is protected")
 298  	}
 299  }
 300  
 301  // TestHandlePolicyAdminFollowListUpdate tests follow list update from admin
 302  func TestHandlePolicyAdminFollowListUpdate(t *testing.T) {
 303  	adminSigner := p8k.MustNew()
 304  	if err := adminSigner.Generate(); err != nil {
 305  		t.Fatalf("Failed to generate admin keypair: %v", err)
 306  	}
 307  	adminHex := hex.Enc(adminSigner.Pub())
 308  
 309  	listener, db, cleanup := setupPolicyTestListener(t, adminHex)
 310  	defer cleanup()
 311  
 312  	// Create a kind 3 follow list event from admin
 313  	ev := event.New()
 314  	ev.CreatedAt = time.Now().Unix()
 315  	ev.Kind = kind.FollowList.K
 316  	ev.Content = []byte("")
 317  	ev.Tags = tag.NewS()
 318  
 319  	// Add some follows
 320  	follow1Hex := "1111111111111111111111111111111111111111111111111111111111111111"
 321  	follow2Hex := "2222222222222222222222222222222222222222222222222222222222222222"
 322  	ev.Tags.Append(tag.NewFromAny("p", follow1Hex))
 323  	ev.Tags.Append(tag.NewFromAny("p", follow2Hex))
 324  
 325  	if err := ev.Sign(adminSigner); err != nil {
 326  		t.Fatalf("Failed to sign event: %v", err)
 327  	}
 328  
 329  	// Save the event to database first
 330  	if _, err := db.SaveEvent(listener.ctx, ev); err != nil {
 331  		t.Fatalf("Failed to save follow list event: %v", err)
 332  	}
 333  
 334  	// Handle the follow list update
 335  	err := listener.HandlePolicyAdminFollowListUpdate(ev)
 336  	if err != nil {
 337  		t.Errorf("Expected success but got error: %v", err)
 338  	}
 339  
 340  	// Verify follows were added
 341  	follow1Bin, _ := hex.Dec(follow1Hex)
 342  	follow2Bin, _ := hex.Dec(follow2Hex)
 343  
 344  	if !listener.policyManager.IsPolicyFollow(follow1Bin) {
 345  		t.Error("Follow 1 should have been added to policy follows")
 346  	}
 347  	if !listener.policyManager.IsPolicyFollow(follow2Bin) {
 348  		t.Error("Follow 2 should have been added to policy follows")
 349  	}
 350  }
 351  
 352  // TestIsPolicyAdminFollowListEvent tests detection of admin follow list events
 353  func TestIsPolicyAdminFollowListEvent(t *testing.T) {
 354  	adminSigner := p8k.MustNew()
 355  	if err := adminSigner.Generate(); err != nil {
 356  		t.Fatalf("Failed to generate admin keypair: %v", err)
 357  	}
 358  	adminHex := hex.Enc(adminSigner.Pub())
 359  
 360  	nonAdminSigner := p8k.MustNew()
 361  	if err := nonAdminSigner.Generate(); err != nil {
 362  		t.Fatalf("Failed to generate non-admin keypair: %v", err)
 363  	}
 364  
 365  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 366  	defer cleanup()
 367  
 368  	// Test admin's kind 3 event
 369  	adminFollowEv := event.New()
 370  	adminFollowEv.Kind = kind.FollowList.K
 371  	adminFollowEv.Tags = tag.NewS()
 372  	if err := adminFollowEv.Sign(adminSigner); err != nil {
 373  		t.Fatalf("Failed to sign event: %v", err)
 374  	}
 375  
 376  	if !listener.IsPolicyAdminFollowListEvent(adminFollowEv) {
 377  		t.Error("Should detect admin's follow list event")
 378  	}
 379  
 380  	// Test non-admin's kind 3 event
 381  	nonAdminFollowEv := event.New()
 382  	nonAdminFollowEv.Kind = kind.FollowList.K
 383  	nonAdminFollowEv.Tags = tag.NewS()
 384  	if err := nonAdminFollowEv.Sign(nonAdminSigner); err != nil {
 385  		t.Fatalf("Failed to sign event: %v", err)
 386  	}
 387  
 388  	if listener.IsPolicyAdminFollowListEvent(nonAdminFollowEv) {
 389  		t.Error("Should not detect non-admin's follow list event")
 390  	}
 391  
 392  	// Test admin's non-kind-3 event
 393  	adminOtherEv := event.New()
 394  	adminOtherEv.Kind = 1 // Kind 1, not follow list
 395  	adminOtherEv.Tags = tag.NewS()
 396  	if err := adminOtherEv.Sign(adminSigner); err != nil {
 397  		t.Fatalf("Failed to sign event: %v", err)
 398  	}
 399  
 400  	if listener.IsPolicyAdminFollowListEvent(adminOtherEv) {
 401  		t.Error("Should not detect admin's non-follow-list event")
 402  	}
 403  }
 404  
 405  // TestIsPolicyConfigEvent tests detection of policy config events
 406  func TestIsPolicyConfigEvent(t *testing.T) {
 407  	signer := p8k.MustNew()
 408  	if err := signer.Generate(); err != nil {
 409  		t.Fatalf("Failed to generate keypair: %v", err)
 410  	}
 411  
 412  	// Kind 12345 event
 413  	policyEv := event.New()
 414  	policyEv.Kind = kind.PolicyConfig.K
 415  	policyEv.Tags = tag.NewS()
 416  	if err := policyEv.Sign(signer); err != nil {
 417  		t.Fatalf("Failed to sign event: %v", err)
 418  	}
 419  
 420  	if !IsPolicyConfigEvent(policyEv) {
 421  		t.Error("Should detect kind 12345 as policy config event")
 422  	}
 423  
 424  	// Non-policy event
 425  	otherEv := event.New()
 426  	otherEv.Kind = 1
 427  	otherEv.Tags = tag.NewS()
 428  	if err := otherEv.Sign(signer); err != nil {
 429  		t.Fatalf("Failed to sign event: %v", err)
 430  	}
 431  
 432  	if IsPolicyConfigEvent(otherEv) {
 433  		t.Error("Should not detect kind 1 as policy config event")
 434  	}
 435  }
 436  
 437  // TestMessageProcessingPauseDuringPolicyUpdate tests that message processing is paused
 438  func TestMessageProcessingPauseDuringPolicyUpdate(t *testing.T) {
 439  	adminSigner := p8k.MustNew()
 440  	if err := adminSigner.Generate(); err != nil {
 441  		t.Fatalf("Failed to generate admin keypair: %v", err)
 442  	}
 443  	adminHex := hex.Enc(adminSigner.Pub())
 444  
 445  	listener, _, cleanup := setupPolicyTestListener(t, adminHex)
 446  	defer cleanup()
 447  
 448  	// Track if pause was called
 449  	pauseCalled := false
 450  	resumeCalled := false
 451  
 452  	// We can't easily mock the mutex, but we can verify the policy update succeeds
 453  	// which implies the pause/resume cycle completed
 454  	// Note: policy_admins must stay the same (protected field)
 455  	newPolicyJSON := `{
 456  		"default_policy": "allow",
 457  		"policy_admins": ["` + adminHex + `"],
 458  		"kind": {"whitelist": [1, 3, 5, 7]}
 459  	}`
 460  	ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
 461  
 462  	err := listener.HandlePolicyConfigUpdate(ev)
 463  	if err != nil {
 464  		t.Errorf("Policy update failed: %v", err)
 465  	}
 466  
 467  	// If we got here without deadlock, the pause/resume worked
 468  	_ = pauseCalled
 469  	_ = resumeCalled
 470  
 471  	// Verify policy was actually updated (kind whitelist was extended)
 472  	if listener.policyManager.DefaultPolicy != "allow" {
 473  		t.Error("Policy should have been updated")
 474  	}
 475  }
 476