package policy import ( "testing" "git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/interfaces/signer/p8k" ) // TestReadAllowLogic tests the correct semantics of ReadAllow: // ReadAllow should control WHO can read events of a kind, // not which event authors can be read. func TestReadAllowLogic(t *testing.T) { // Set up: Create 3 different users // - alice: will author an event // - bob: will be allowed to read (in ReadAllow list) // - charlie: will NOT be allowed to read (not in ReadAllow list) aliceSigner, alicePubkey := generateTestKeypair(t) _, bobPubkey := generateTestKeypair(t) _, charliePubkey := generateTestKeypair(t) // Create an event authored by Alice (kind 30166) aliceEvent := createTestEvent(t, aliceSigner, "server heartbeat", 30166) // Create policy: Only Bob can READ kind 30166 events policy := &P{ DefaultPolicy: "allow", Rules: map[int]Rule{ 30166: { Description: "Private server heartbeat events", ReadAllow: []string{hex.Enc(bobPubkey)}, // Only Bob can read }, }, } // Test 1: Bob (who is in ReadAllow) should be able to READ Alice's event t.Run("allowed_reader_can_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, bobPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Bob should be allowed to READ Alice's event (Bob is in ReadAllow list)") } }) // Test 2: Charlie (who is NOT in ReadAllow) should NOT be able to READ Alice's event t.Run("disallowed_reader_cannot_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Charlie should NOT be allowed to READ Alice's event (Charlie is not in ReadAllow list)") } }) // Test 3: Alice (the author) should NOT be able to READ her own event if she's not in ReadAllow t.Run("author_not_in_readallow_cannot_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, 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 her own event (Alice is not in ReadAllow list)") } }) // Test 4: Unauthenticated user should NOT be able to READ t.Run("unauthenticated_cannot_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, nil, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Unauthenticated user should NOT be allowed to READ (not in ReadAllow list)") } }) } // TestReadDenyLogic tests the correct semantics of ReadDeny: // ReadDeny should control WHO cannot read events of a kind, // not which event authors cannot be read. func TestReadDenyLogic(t *testing.T) { // Set up: Create 3 different users aliceSigner, alicePubkey := generateTestKeypair(t) _, bobPubkey := generateTestKeypair(t) _, charliePubkey := generateTestKeypair(t) // Create an event authored by Alice aliceEvent := createTestEvent(t, aliceSigner, "test content", 1) // Create policy: Charlie cannot READ kind 1 events (but others can) policy := &P{ DefaultPolicy: "allow", Rules: map[int]Rule{ 1: { Description: "Test events", ReadDeny: []string{hex.Enc(charliePubkey)}, // Charlie cannot read }, }, } // Test 1: Bob (who is NOT in ReadDeny) should be able to READ Alice's event t.Run("non_denied_reader_can_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, bobPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Bob should be allowed to READ Alice's event (Bob is not in ReadDeny list)") } }) // Test 2: Charlie (who IS in ReadDeny) should NOT be able to READ Alice's event t.Run("denied_reader_cannot_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Charlie should NOT be allowed to READ Alice's event (Charlie is in ReadDeny list)") } }) // Test 3: Alice (the author, not in ReadDeny) should be able to READ her own event t.Run("author_not_denied_can_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, alicePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Alice should be allowed to READ her own event (Alice is not in ReadDeny list)") } }) } // TestSamplePolicyFromUser tests the exact policy configuration provided by the user func TestSamplePolicyFromUser(t *testing.T) { policyJSON := []byte(`{ "kind": { "whitelist": [4678, 10306, 30520, 30919, 30166] }, "rules": { "4678": { "description": "Zenotp message events", "write_allow": [ "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5", "e4101949fb0367c72f5105fc9bd810cde0e0e0f950da26c1f47a6af5f77ded31", "3f5fefcdc3fb41f3b299732acad7dc9c3649e8bde97d4f238380dde547b5e0e0" ], "privileged": true }, "10306": { "description": "End user whitelist change requests", "read_allow": [ "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5" ], "privileged": true }, "30520": { "description": "End user whitelist events", "write_allow": [ "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5" ], "privileged": true }, "30919": { "description": "Customer indexing events", "write_allow": [ "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5" ], "privileged": true }, "30166": { "description": "Private server heartbeat events", "write_allow": [ "4d13154d82477a2d2e07a5c0d52def9035fdf379ae87cd6f0a5fb87801a4e5e4", "e400106ed10310ea28b039e81824265434bf86ece58722655c7a98f894406112" ], "read_allow": [ "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5", "4d13154d82477a2d2e07a5c0d52def9035fdf379ae87cd6f0a5fb87801a4e5e4", "e400106ed10310ea28b039e81824265434bf86ece58722655c7a98f894406112" ] } } }`) policy, err := New(policyJSON) if err != nil { t.Fatalf("Failed to create policy: %v", err) } // Define the test users adminPubkeyHex := "04eeb1ed409c0b9205e722f8bf1780f553b61876ef323aff16c9f80a9d8ee9f5" server1PubkeyHex := "4d13154d82477a2d2e07a5c0d52def9035fdf379ae87cd6f0a5fb87801a4e5e4" server2PubkeyHex := "e400106ed10310ea28b039e81824265434bf86ece58722655c7a98f894406112" adminPubkey, _ := hex.Dec(adminPubkeyHex) server1Pubkey, _ := hex.Dec(server1PubkeyHex) server2Pubkey, _ := hex.Dec(server2PubkeyHex) // Create a random user not in any allow list randomSigner, randomPubkey := generateTestKeypair(t) // Test Kind 30166 (Private server heartbeat events) t.Run("kind_30166_read_access", func(t *testing.T) { // We can't sign with the exact pubkey without the private key, // so we'll create a generic event and manually set the pubkey for testing heartbeatEvent := createTestEvent(t, randomSigner, "heartbeat data", 30166) heartbeatEvent.Pubkey = server1Pubkey // Set to server1's pubkey // Test 1: Admin (in read_allow) should be able to READ the heartbeat allowed, err := policy.CheckPolicy("read", heartbeatEvent, adminPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Admin should be allowed to READ kind 30166 events (admin is in read_allow list)") } // Test 2: Server1 (in read_allow) should be able to READ the heartbeat allowed, err = policy.CheckPolicy("read", heartbeatEvent, server1Pubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Server1 should be allowed to READ kind 30166 events (server1 is in read_allow list)") } // Test 3: Server2 (in read_allow) should be able to READ the heartbeat allowed, err = policy.CheckPolicy("read", heartbeatEvent, server2Pubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Server2 should be allowed to READ kind 30166 events (server2 is in read_allow list)") } // Test 4: Random user (NOT in read_allow) should NOT be able to READ the heartbeat allowed, err = policy.CheckPolicy("read", heartbeatEvent, 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 30166 events (not in read_allow list)") } // Test 5: Unauthenticated user should NOT be able to READ (privileged + read_allow) allowed, err = policy.CheckPolicy("read", heartbeatEvent, nil, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Unauthenticated user should NOT be allowed to READ kind 30166 events (privileged)") } }) // Test Kind 10306 (End user whitelist change requests) t.Run("kind_10306_read_access", func(t *testing.T) { // Create an event authored by a random user requestEvent := createTestEvent(t, randomSigner, "whitelist change request", 10306) // Add admin to p tag to satisfy privileged requirement addPTag(requestEvent, adminPubkey) // Test 1: Admin (in read_allow) should be able to READ the request allowed, err := policy.CheckPolicy("read", requestEvent, adminPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Admin should be allowed to READ kind 10306 events (admin is in read_allow list)") } // Test 2: Server1 (NOT in read_allow for kind 10306) should NOT be able to READ // Even though server1 might be allowed for kind 30166 allowed, err = policy.CheckPolicy("read", requestEvent, server1Pubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Server1 should NOT be allowed to READ kind 10306 events (not in read_allow list for this kind)") } // 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 be allowed to READ kind 10306 events (author - privileged check passes, OR logic)") } }) } // TestReadAllowWithPrivileged tests interaction between read_allow and privileged func TestReadAllowWithPrivileged(t *testing.T) { aliceSigner, alicePubkey := generateTestKeypair(t) _, bobPubkey := generateTestKeypair(t) _, charliePubkey := generateTestKeypair(t) // Create policy: Kind 100 is privileged AND has read_allow policy := &P{ DefaultPolicy: "allow", Rules: map[int]Rule{ 100: { Description: "Privileged with read_allow", Privileged: true, ReadAllow: []string{hex.Enc(bobPubkey)}, // Only Bob can read }, }, } // Create event authored by Alice, with Bob in p tag ev := createTestEvent(t, aliceSigner, "secret message", 100) addPTag(ev, bobPubkey) // Test 1: Bob (in ReadAllow AND in p tag) should be able to READ t.Run("bob_in_readallow_and_ptag", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", ev, bobPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Bob should be allowed to READ (in ReadAllow AND satisfies privileged)") } }) // 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 be allowed to READ (privileged check passes - she's the author, OR logic)") } }) // Test 3: Charlie (NOT in ReadAllow, NOT in p tag) should NOT be able to READ t.Run("charlie_not_authorized", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", ev, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Charlie should NOT be allowed to READ (not in ReadAllow)") } }) // Test 4: Create event with Charlie in p tag but Charlie not in ReadAllow evWithCharlie := createTestEvent(t, aliceSigner, "message for charlie", 100) addPTag(evWithCharlie, charliePubkey) t.Run("charlie_in_ptag_but_not_readallow", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", evWithCharlie, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Charlie SHOULD be allowed to READ (privileged check passes - he's in p-tag, OR logic)") } }) } // TestReadAllowWriteAllowIndependent verifies that read_allow and write_allow are independent func TestReadAllowWriteAllowIndependent(t *testing.T) { aliceSigner, alicePubkey := generateTestKeypair(t) bobSigner, bobPubkey := generateTestKeypair(t) _, charliePubkey := generateTestKeypair(t) // Create policy: // - Alice can WRITE // - Bob can READ // - Charlie can do neither policy := &P{ DefaultPolicy: "allow", Rules: map[int]Rule{ 200: { Description: "Write/Read separation test", WriteAllow: []string{hex.Enc(alicePubkey)}, // Only Alice can write ReadAllow: []string{hex.Enc(bobPubkey)}, // Only Bob can read }, }, } // Alice creates an event aliceEvent := createTestEvent(t, aliceSigner, "alice's message", 200) // Test 1: Alice can WRITE her own event t.Run("alice_can_write", func(t *testing.T) { allowed, err := policy.CheckPolicy("write", aliceEvent, alicePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Alice should be allowed to WRITE (in WriteAllow)") } }) // Test 2: Alice CANNOT READ her own event (not in ReadAllow) t.Run("alice_cannot_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, 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, even though she wrote it)") } }) // Bob creates an event (will be denied on write) bobEvent := createTestEvent(t, bobSigner, "bob's message", 200) // Test 3: Bob CANNOT WRITE (not in WriteAllow) t.Run("bob_cannot_write", func(t *testing.T) { allowed, err := policy.CheckPolicy("write", bobEvent, bobPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Bob should NOT be allowed to WRITE (not in WriteAllow)") } }) // Test 4: Bob CAN READ Alice's event (in ReadAllow) t.Run("bob_can_read", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", aliceEvent, bobPubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if !allowed { t.Error("Bob should be allowed to READ Alice's event (in ReadAllow)") } }) // Test 5: Charlie cannot write or read t.Run("charlie_cannot_write_or_read", func(t *testing.T) { // Create an event authored by Charlie charlieSigner := p8k.MustNew() charlieSigner.Generate() charlieEvent := createTestEvent(t, charlieSigner, "charlie's message", 200) charlieEvent.Pubkey = charliePubkey // Set to Charlie's pubkey // Charlie's event should be denied for write (Charlie not in WriteAllow) allowed, err := policy.CheckPolicy("write", charlieEvent, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Charlie should NOT be allowed to WRITE events of kind 200 (not in WriteAllow)") } // Charlie should not be able to READ Alice's event (not in ReadAllow) allowed, err = policy.CheckPolicy("read", aliceEvent, charliePubkey, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Charlie should NOT be allowed to READ (not in ReadAllow)") } }) } // TestReadAccessEdgeCases tests edge cases like nil pubkeys func TestReadAccessEdgeCases(t *testing.T) { aliceSigner, _ := generateTestKeypair(t) policy := &P{ DefaultPolicy: "allow", Rules: map[int]Rule{ 300: { Description: "Test edge cases", ReadAllow: []string{"somepubkey"}, // Non-empty ReadAllow }, }, } event := createTestEvent(t, aliceSigner, "test", 300) // Test 1: Nil loggedInPubkey with ReadAllow should be denied t.Run("nil_pubkey_with_readallow", func(t *testing.T) { allowed, err := policy.CheckPolicy("read", event, nil, "127.0.0.1") if err != nil { t.Fatalf("Unexpected error: %v", err) } if allowed { t.Error("Nil pubkey should NOT be allowed when ReadAllow is set") } }) // Test 2: Verify hex.Enc(nil) doesn't accidentally match anything t.Run("hex_enc_nil_no_match", func(t *testing.T) { emptyStringHex := hex.Enc(nil) t.Logf("hex.Enc(nil) = %q (len=%d)", emptyStringHex, len(emptyStringHex)) // Verify it's empty string if emptyStringHex != "" { t.Errorf("Expected hex.Enc(nil) to be empty string, got %q", emptyStringHex) } }) }