diff --git a/pkg/policy/kind_whitelist_test.go b/pkg/policy/kind_whitelist_test.go new file mode 100644 index 0000000..2837f74 --- /dev/null +++ b/pkg/policy/kind_whitelist_test.go @@ -0,0 +1,317 @@ +package policy + +import ( + "testing" + + "next.orly.dev/pkg/encoders/hex" +) + +// TestKindWhitelistComprehensive verifies that kind whitelisting properly rejects +// unlisted kinds in all scenarios: explicit whitelist, implicit whitelist (rules), and combinations +func TestKindWhitelistComprehensive(t *testing.T) { + testSigner, testPubkey := generateTestKeypair(t) + + t.Run("Explicit Whitelist - kind IN whitelist, HAS rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", // Changed to allow so rules without constraints allow by default + Kind: Kinds{ + Whitelist: []int{1, 3, 5}, // Explicit whitelist + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 3: {Description: "Rule for kind 3"}, + 5: {Description: "Rule for kind 5"}, + }, + } + + event := createTestEvent(t, testSigner, "test", 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 (in whitelist, has rule, passes rule check)") + } + }) + + t.Run("Explicit Whitelist - kind IN whitelist, NO rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Whitelist: []int{1, 3, 5}, // Explicit whitelist + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + // Kind 3 has no rule + }, + } + + event := createTestEvent(t, testSigner, "test", 3) + 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 3 should be ALLOWED (in whitelist, no rule, default policy is allow)") + } + }) + + t.Run("Explicit Whitelist - kind NOT in whitelist, HAS rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Whitelist: []int{1, 3, 5}, // Explicit whitelist - kind 10 NOT included + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 10: {Description: "Rule for kind 10"}, // Has rule but not in whitelist! + }, + } + + event := createTestEvent(t, testSigner, "test", 10) + 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 10 should be REJECTED (NOT in whitelist, even though it has a rule)") + } + }) + + t.Run("Explicit Whitelist - kind NOT in whitelist, NO rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Whitelist: []int{1, 3, 5}, // Explicit whitelist + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + }, + } + + event := createTestEvent(t, testSigner, "test", 99) + 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 99 should be REJECTED (NOT in whitelist)") + } + }) + + t.Run("Implicit Whitelist (rules) - kind HAS rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", // Changed to allow so rules without constraints allow by default + // No explicit whitelist + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 3: {Description: "Rule for kind 3"}, + }, + } + + event := createTestEvent(t, testSigner, "test", 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 (has rule, implicit whitelist)") + } + }) + + t.Run("Implicit Whitelist (rules) - kind NO rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + // No explicit whitelist + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 3: {Description: "Rule for kind 3"}, + }, + } + + event := createTestEvent(t, testSigner, "test", 99) + 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 99 should be REJECTED (no rule, implicit whitelist mode)") + } + }) + + t.Run("Explicit Whitelist + Global Rule - kind NOT in whitelist", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Whitelist: []int{1, 3, 5}, // Explicit whitelist + }, + Global: Rule{ + Description: "Global rule applies to all kinds", + WriteAllow: []string{hex.Enc(testPubkey)}, + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + }, + } + + // Even with global rule, kind not in whitelist should be rejected + event := createTestEvent(t, testSigner, "test", 99) + 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 99 should be REJECTED (NOT in whitelist, even with global rule)") + } + }) + + t.Run("Blacklist + Rules - kind in blacklist but has rule", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Blacklist: []int{10, 20}, // Blacklist + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 10: {Description: "Rule for kind 10"}, // Has rule but blacklisted! + }, + } + + // Kind 10 is blacklisted, should be rejected + event := createTestEvent(t, testSigner, "test", 10) + 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 10 should be REJECTED (in blacklist)") + } + }) + + t.Run("Blacklist + Rules - kind NOT in blacklist but NO rule (implicit whitelist)", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Blacklist: []int{10, 20}, // Blacklist + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 3: {Description: "Rule for kind 3"}, + }, + } + + // Kind 99 is not blacklisted but has no rule + // With blacklist present + rules, implicit whitelist applies + event := createTestEvent(t, testSigner, "test", 99) + 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 99 should be REJECTED (not in blacklist but no rule, implicit whitelist)") + } + }) + + t.Run("Whitelist takes precedence over Blacklist", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", + Kind: Kinds{ + Whitelist: []int{1, 3, 5, 10}, // Whitelist includes 10 + Blacklist: []int{10, 20}, // Blacklist also includes 10 + }, + Rules: map[int]Rule{ + 1: {Description: "Rule for kind 1"}, + 10: {Description: "Rule for kind 10"}, + }, + } + + // Kind 10 is in BOTH whitelist and blacklist - whitelist should win + event := createTestEvent(t, testSigner, "test", 10) + 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 10 should be ALLOWED (whitelist takes precedence over blacklist)") + } + }) +} + +// TestKindWhitelistRealWorld tests real-world scenarios from the documentation +func TestKindWhitelistRealWorld(t *testing.T) { + testSigner, testPubkey := generateTestKeypair(t) + _, otherPubkey := generateTestKeypair(t) + + t.Run("Real World: Only allow kinds 1, 3, 30023", func(t *testing.T) { + policy := &P{ + DefaultPolicy: "allow", // Allow by default for kinds in whitelist + Kind: Kinds{ + Whitelist: []int{1, 3, 30023}, + }, + Rules: map[int]Rule{ + 1: { + Description: "Text notes", + // No WriteAllow = anyone authenticated can write + }, + 3: { + Description: "Contact lists", + // No WriteAllow = anyone authenticated can write + }, + 30023: { + Description: "Long-form content", + WriteAllow: []string{hex.Enc(testPubkey)}, // Only specific user can write + }, + }, + } + + // Test kind 1 (allowed) + event1 := createTestEvent(t, testSigner, "Hello world", 1) + allowed, err := policy.CheckPolicy("write", event1, testPubkey, "127.0.0.1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !allowed { + t.Error("Kind 1 should be ALLOWED") + } + + // Test kind 4 (NOT in whitelist, should be rejected) + event4 := createTestEvent(t, testSigner, "DM", 4) + allowed, err = policy.CheckPolicy("write", event4, testPubkey, "127.0.0.1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if allowed { + t.Error("Kind 4 should be REJECTED (not in whitelist)") + } + + // Test kind 30023 by authorized user (allowed) + event30023Auth := createTestEvent(t, testSigner, "Article", 30023) + allowed, err = policy.CheckPolicy("write", event30023Auth, testPubkey, "127.0.0.1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !allowed { + t.Error("Kind 30023 should be ALLOWED for authorized user") + } + + // Test kind 30023 by unauthorized user (should fail rule check) + event30023Unauth := createTestEvent(t, testSigner, "Article", 30023) + allowed, err = policy.CheckPolicy("write", event30023Unauth, otherPubkey, "127.0.0.1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if allowed { + t.Error("Kind 30023 should be REJECTED for unauthorized user") + } + + // Test kind 9735 (NOT in whitelist, should be rejected even with valid signature) + event9735 := createTestEvent(t, testSigner, "Zap", 9735) + allowed, err = policy.CheckPolicy("write", event9735, testPubkey, "127.0.0.1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if allowed { + t.Error("Kind 9735 should be REJECTED (not in whitelist)") + } + }) +} diff --git a/pkg/version/version b/pkg/version/version index e362ce5..f7dd314 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.29.11 \ No newline at end of file +v0.29.13 \ No newline at end of file