diff --git a/tests/federation_acl_test.go b/tests/federation_acl_test.go index c7f4dae5..06e8754b 100644 --- a/tests/federation_acl_test.go +++ b/tests/federation_acl_test.go @@ -12,6 +12,7 @@ import ( "github.com/matrix-org/complement/runtime" "github.com/matrix-org/complement/should" "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/tidwall/gjson" ) // Test for https://github.com/matrix-org/dendrite/issues/3004 @@ -120,3 +121,115 @@ func TestACLs(t *testing.T) { ) } } + +// MSC4163: TestACLsForEDUs checks that ACLs are applied to EDUs (typing notifications and read receipts) +func TestACLsForEDUs(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) + deployment := complement.Deploy(t, 3) + defer deployment.Destroy(t) + + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + bob := deployment.Register(t, "hs2", helpers.RegistrationOpts{}) + charlie := deployment.Register(t, "hs3", helpers.RegistrationOpts{}) + + // Create a room where hs2 will be blocked by ACL + roomID := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"}) + aliceSince := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID)) + + bob.MustJoinRoom(t, roomID, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + }) + aliceSince = alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncJoinedTo(bob.UserID, roomID)) + bobSince := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) + + // Create a sentinel room without ACLs to confirm federation is working + sentinelRoom := alice.MustCreateRoom(t, map[string]interface{}{"preset": "public_chat"}) + aliceSince = alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncJoinedTo(alice.UserID, sentinelRoom)) + bob.MustJoinRoom(t, sentinelRoom, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + }) + charlie.MustJoinRoom(t, sentinelRoom, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + }) + aliceSince = alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, + client.SyncJoinedTo(bob.UserID, sentinelRoom), + client.SyncJoinedTo(charlie.UserID, sentinelRoom), + ) + + // Block hs2 from participating in room with id roomID + stateKey := "" + aclEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.server_acl", + Sender: alice.UserID, + StateKey: &stateKey, + Content: map[string]interface{}{ + "allow": []string{"*"}, + "allow_ip_literals": true, + "deny": []string{ + string(deployment.GetFullyQualifiedHomeserverName(t, "hs2")), + }, + }, + }) + // Wait for the ACL to reach hs2 before sending EDUs + bob.MustSyncUntil(t, client.SyncReq{Since: bobSince}, client.SyncTimelineHasEventID(roomID, aclEventID)) + + // Join charlie to roomID after the ACL is set up + charlie.MustJoinRoom(t, roomID, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + }) + aliceSince = alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, client.SyncJoinedTo(charlie.UserID, roomID)) + charlieSince := charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(charlie.UserID, roomID)) + + // Bob starts typing in both rooms; typing in roomID should be dropped by ACLs + bob.SendTyping(t, roomID, true, 10000) + bob.SendTyping(t, sentinelRoom, true, 10000) + + // Bob sets read receipts on both rooms; receipts in roomID should be dropped by ACLs + bob.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "read_markers"}, + client.WithJSONBody(t, map[string]interface{}{"m.read": aclEventID})) + + // Send a sentinel message to sentinelRoom to use as a receipt anchor + sentinelEventID := bob.SendEventSynced(t, sentinelRoom, b.Event{ + Type: "m.room.message", + Sender: bob.UserID, + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "sentinel", + }, + }) + bob.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", sentinelRoom, "read_markers"}, + client.WithJSONBody(t, map[string]interface{}{"m.read": sentinelEventID})) + + // Wait for the sentinel message and EDUs in sentinelRoom to arrive, proving the federation + // transaction containing hs2's EDUs was processed + alice.MustSyncUntil(t, client.SyncReq{Since: aliceSince}, + client.SyncTimelineHasEventID(sentinelRoom, sentinelEventID), + client.SyncEphemeralHas(sentinelRoom, func(result gjson.Result) bool { + return result.Get("type").Str == "m.receipt" + }), + client.SyncEphemeralHas(sentinelRoom, func(result gjson.Result) bool { + return result.Get("type").Str == "m.typing" + }), + ) + charlie.MustSyncUntil(t, client.SyncReq{Since: charlieSince}, + client.SyncTimelineHasEventID(sentinelRoom, sentinelEventID), + client.SyncEphemeralHas(sentinelRoom, func(result gjson.Result) bool { + return result.Get("type").Str == "m.receipt" + }), + client.SyncEphemeralHas(sentinelRoom, func(result gjson.Result) bool { + return result.Get("type").Str == "m.typing" + }), + ) + + for _, user := range []*client.CSAPI{alice, charlie} { + syncResp, _ := user.MustSync(t, client.SyncReq{}) + + // No typing or read receipts from the blocked server should appear in room with id roomID + ephemerals := syncResp.Get("rooms.join." + client.GjsonEscape(roomID) + ".ephemeral") + must.MatchGJSON(t, ephemerals, match.JSONKeyArrayOfSize("events", 0)) + + // sentinelRoom should have received the read receipt and typing notification + ephemerals = syncResp.Get("rooms.join." + client.GjsonEscape(sentinelRoom) + ".ephemeral") + must.MatchGJSON(t, ephemerals, match.JSONKeyArrayOfSize("events", 2)) + } +}